Szablony C ++, które akceptują tylko określone typy


158

W Javie możesz zdefiniować klasę ogólną, która akceptuje tylko typy rozszerzające wybraną przez Ciebie klasę, np .:

public class ObservableList<T extends List> {
  ...
}

Odbywa się to za pomocą słowa kluczowego „extends”.

Czy istnieje jakiś prosty odpowiednik tego słowa kluczowego w C ++?


dość stare pytanie ... Czuję, że brakuje tutaj (również z odpowiedzi), że generyczne programy Java nie są tak naprawdę odpowiednikiem szablonów w C ++. Istnieją podobieństwa, ale imho należy uważać przy bezpośrednim tłumaczeniu rozwiązania Java na C ++, tylko po to, aby zdać sobie sprawę, że mogą one być stworzone do różnych rodzajów problemów;)
idclev 463035818

Odpowiedzi:


104

Sugeruję użycie statycznej funkcji asercji Boost w połączeniu z is_base_ofbiblioteką cech typu Boost:

template<typename T>
class ObservableList {
    BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
    ...
};

W innych, prostszych przypadkach możesz po prostu zadeklarować szablon globalny, ale zdefiniować go (jawnie lub częściowo) tylko dla prawidłowych typów:

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
    ...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
    ...
};

// All other types are invalid, and will cause linker error messages.

[Drobne EDYCJA 12.06.2013: Użycie zadeklarowanego, ale niezdefiniowanego szablonu spowoduje wyświetlenie komunikatów o błędach przez konsolidator , a nie kompilator.]


Statyczne aserty też są fajne. :)
macbirdie

5
@John: Obawiam się, że specjalizacja pasowałaby tylko myBaseTypedokładnie. Przed odrzuceniem Boost powinieneś wiedzieć, że większość z nich to kod szablonu zawierający tylko nagłówki - więc nie ma kosztu pamięci ani czasu w czasie wykonywania dla rzeczy, których nie używasz. Również konkretne rzeczy, których użyjesz tutaj ( BOOST_STATIC_ASSERT()i is_base_of<>), mogą być zaimplementowane przy użyciu tylko deklaracji (tj. Bez rzeczywistych definicji funkcji lub zmiennych), więc nie zajmują również miejsca ani czasu.
j_random_hacker,

50
Nadszedł C ++ 11. Teraz możemy użyć static_assert(std::is_base_of<List, T>::value, "T must extend list").
Siyuan Ren

2
BTW, powodem, dla którego podwójny nawias jest konieczny, jest to, że BOOST_STATIC_ASSERT jest makrem, a dodatkowy nawias uniemożliwia preprocesorowi interpretację przecinka w argumentach funkcji is_base_of jako drugiego argumentu makra.
jfritz42

1
@Andreyua: Naprawdę nie rozumiem, czego brakuje. Możesz spróbować zadeklarować zmienną my_template<int> x;lub my_template<float**> y;i sprawdzić, czy kompilator na to zezwala, a następnie zadeklarować zmienną my_template<char> z;i sprawdzić, czy nie.
j_random_hacker

134

Zwykle jest to nieuzasadnione w C ++, jak zauważyły ​​inne odpowiedzi. W C ++ mamy tendencję do definiowania typów ogólnych na podstawie innych ograniczeń innych niż „dziedziczy z tej klasy”. Jeśli naprawdę chciałeś to zrobić, możesz to zrobić w C ++ 11 i <type_traits>:

#include <type_traits>

template<typename T>
class observable_list {
    static_assert(std::is_base_of<list, T>::value, "T must inherit from list");
    // code here..
};

To jednak łamie wiele koncepcji, których ludzie oczekują w C ++. Lepiej jest używać sztuczek, takich jak definiowanie własnych cech. Na przykład, może observable_listchce przyjąć każdy rodzaj kontenera, który ma typedefs const_iteratororaz begini endfunkcję członka, który powraca const_iterator. Jeśli ograniczysz to do klas, które dziedziczą listpo tym czasie, użytkownik, który ma własny typ, który nie dziedziczy, listale udostępnia te funkcje składowe, a typedefs nie będzie mógł użyć twojego observable_list.

Istnieją dwa rozwiązania tego problemu, jednym z nich jest nie ograniczanie niczego i poleganie na pisaniu na klawiaturze. Dużą wadą tego rozwiązania jest to, że wiąże się ono z ogromną liczbą błędów, które mogą być trudne do zrozumienia dla użytkowników. Innym rozwiązaniem jest zdefiniowanie cech w celu ograniczenia podanego typu w celu spełnienia wymagań interfejsu. Dużą wadą tego rozwiązania jest to, że wymaga dodatkowego pisania, co może być postrzegane jako denerwujące. Jednak pozytywną stroną jest to, że będziesz w stanie pisać własne komunikaty o błędach a la static_assert.

Dla kompletności podano rozwiązanie powyższego przykładu:

#include <type_traits>

template<typename...>
struct void_ {
    using type = void;
};

template<typename... Args>
using Void = typename void_<Args...>::type;

template<typename T, typename = void>
struct has_const_iterator : std::false_type {};

template<typename T>
struct has_const_iterator<T, Void<typename T::const_iterator>> : std::true_type {};

struct has_begin_end_impl {
    template<typename T, typename Begin = decltype(std::declval<const T&>().begin()),
                         typename End   = decltype(std::declval<const T&>().end())>
    static std::true_type test(int);
    template<typename...>
    static std::false_type test(...);
};

template<typename T>
struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};

template<typename T>
class observable_list {
    static_assert(has_const_iterator<T>::value, "Must have a const_iterator typedef");
    static_assert(has_begin_end<T>::value, "Must have begin and end member functions");
    // code here...
};

W powyższym przykładzie pokazano wiele koncepcji, które pokazują funkcje C ++ 11. Niektóre terminy wyszukiwania dla ciekawskich to szablony wariadyczne, SFINAE, wyrażenie SFINAE i cechy typu.


2
Do dziś nie zdawałem sobie sprawy, że szablony C ++ używają pisania kaczego. Trochę dziwne!
Andy

2
Biorąc pod uwagę rozległe ograniczenia polityczne C ++ wprowadzone do C , nie jestem pewien, dlaczego template<class T:list>jest to tak obraźliwa koncepcja. Dzięki za wskazówkę.
bvj

60

Prostym rozwiązaniem, o którym nikt jeszcze nie wspomniał, jest po prostu zignorowanie problemu. Jeśli spróbuję użyć inttypu szablonu jako szablonu w szablonie funkcji, który oczekuje klasy kontenera, takiej jak wektor lub lista, otrzymam błąd kompilacji. Surowy i prosty, ale rozwiązuje problem. Kompilator spróbuje użyć określonego typu, a jeśli to się nie powiedzie, wygeneruje błąd kompilacji.

Jedynym problemem jest to, że wyświetlane komunikaty o błędach będą trudne do odczytania. Niemniej jednak jest to bardzo powszechny sposób. Biblioteka standardowa jest pełna szablonów funkcji lub klas, które oczekują określonego zachowania od typu szablonu i nie robią nic, aby sprawdzić, czy użyte typy są prawidłowe.

Jeśli chcesz ładniejszych komunikatów o błędach (lub chcesz wychwycić przypadki, które nie powodowałyby błędu kompilatora, ale nadal nie mają sensu), możesz, w zależności od tego, jak skomplikowane chcesz to uczynić, użyć statycznego potwierdzenia Boost lub Biblioteka Boost concept_check.

Z aktualnym kompilatorem masz wbudowany plik built_in static_assert, którego można by użyć zamiast tego.


7
Tak, zawsze myślałem, że szablony są najbliższe pisaniu na klawiaturze w C ++. Jeśli ma wszystkie elementy niezbędne do szablonu, można go użyć w szablonie.

@John: Przepraszam, nie mogę tego zrobić. Jaki jest typ Ti skąd jest wywoływany ten kod? Bez kontekstu nie mam szans zrozumieć tego fragmentu kodu. Ale to, co powiedziałem, jest prawdą. Jeśli spróbujesz wywołać toString()typ, który nie ma toStringfunkcji składowej, otrzymasz błąd kompilacji.
jalf

@John: następnym razem, być może powinieneś być nieco mniej skłonny do obniżania głosów, gdy problem jest w twoim kodzie
jalf

@jalf, ok. +1. To była świetna odpowiedź, po prostu próbując zrobić to najlepiej. Przepraszamy za błędne czytanie. Myślałem, że rozmawialiśmy o użyciu typu jako parametru dla klas, a nie dla szablonów funkcji, które, jak przypuszczam, są członkami pierwszego, ale wymagają wywołania, aby kompilator mógł oflagować.
John

13

Możemy użyć std::is_base_ofi std::enable_if:
( static_assertmożna usunąć, powyższe klasy mogą być zaimplementowane na zamówienie lub używane z boost, jeśli nie możemy się odwołać type_traits)

#include <type_traits>
#include <list>

class Base {};
class Derived: public Base {};

#if 0   // wrapper
template <class T> class MyClass /* where T:Base */ {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
};
#elif 0 // base class
template <class T> class MyClass: /* where T:Base */
    protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
};
#elif 1 // list-of
template <class T> class MyClass /* where T:list<Base> */ {
    static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;

};
#endif

int main() {
#if 0   // wrapper or base-class
    MyClass<Derived> derived;
    MyClass<Base> base;
//  error:
    MyClass<int> wrong;
#elif 1 // list-of
    MyClass<std::list<Derived>> derived;
    MyClass<std::list<Base>> base;
//  error:
    MyClass<std::list<int>> wrong;
#endif
//  all of the static_asserts if not commented out
//  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
//  1. inner
//  2. MyClass
//  3. base + value_type
}

13

O ile wiem, nie jest to obecnie możliwe w C ++. Istnieją jednak plany dodania funkcji zwanej „koncepcjami” w nowym standardzie C ++ 0x, która zapewni funkcjonalność, której szukasz. Ten artykuł w Wikipedii o C ++ Concepts wyjaśni to bardziej szczegółowo.

Wiem, że to nie rozwiązuje twojego bezpośredniego problemu, ale jest kilka kompilatorów C ++, które już zaczęły dodawać funkcje z nowego standardu, więc może być możliwe znalezienie kompilatora, który już zaimplementował funkcję koncepcyjną.


4
Koncepcje zostały niestety usunięte ze standardu.
macbirdie,

4
Ograniczenia i koncepcje należy przyjąć dla C ++ 20.
Petr Javorik

Jest to możliwe nawet bez koncepcji, przy użyciu static_asserti SFINAE, jak pokazują inne odpowiedzi. Pozostała kwestia dla kogoś pochodzącego z Javy, C # lub Haskella (...) polega na tym, że kompilator C ++ 20 nie sprawdza definicji pod kątem wymaganych pojęć, co robią Java i C #.
user7610

10

Myślę, że wszystkie wcześniejsze odpowiedzi straciły las z oczu.

Typy Java to nie to samo, co szablony ; używają wymazywania typów , które jest techniką dynamiczną , zamiast polimorfizmu czasu kompilacji , który jest techniką statyczną . Powinno być oczywiste, dlaczego te dwie bardzo różne taktyki nie sprawdzają się dobrze.

Zamiast próbować używać konstrukcji czasu kompilacji do symulacji czasu wykonywania, przyjrzyjmy się, co extendsfaktycznie robi: zgodnie z przepełnieniem stosu i Wikipedii , rozszerzenie jest używane do wskazywania podklas.

C ++ obsługuje również podklasy.

Pokazujesz również klasę kontenera, która używa wymazywania typów w postaci ogólnej i rozszerza się, aby wykonać sprawdzenie typu. W C ++ musisz samodzielnie wykonać maszynę do wymazywania typów, co jest proste: utworzyć wskaźnik do nadklasy.

Umieśćmy to w typedef, aby ułatwić korzystanie z niego, zamiast tworzyć całą klasę, et voila:

typedef std::list<superclass*> subclasses_of_superclass_only_list;

Na przykład:

class Shape { };
class Triangle : public Shape { };

typedef std::list<Shape*> only_shapes_list;
only_shapes_list shapes;

shapes.push_back(new Triangle()); // Works, triangle is kind of shape
shapes.push_back(new int(30)); // Error, int's are not shapes

Wygląda na to, że List jest interfejsem reprezentującym rodzaj kolekcji. Interfejs w C ++ byłby jedynie klasą abstrakcyjną, to znaczy klasą, która implementuje tylko czyste metody wirtualne. Korzystając z tej metody, możesz łatwo zaimplementować przykład Java w C ++, bez żadnych pojęć lub specjalizacji w szablonach. Działałby również tak wolno, jak typy generyczne w stylu Java ze względu na wyszukiwanie wirtualnych tabel, ale często może to być akceptowalna strata.


3
Nie jestem fanem odpowiedzi, które używają zwrotów takich jak „to powinno być oczywiste” lub „wszyscy wiedzą”, a następnie wyjaśniam, co jest oczywiste lub powszechnie znane. Oczywistość odnosi się do kontekstu, doświadczenia i kontekstu doświadczenia. Takie stwierdzenia są z natury niegrzeczne.
Zapisz

2
@DavidLively Jest już około dwóch lat za późno na krytykowanie tej odpowiedzi za etykietę, ale nie zgadzam się z tobą również w tym konkretnym przypadku; Wyjaśniłem, dlaczego te dwie techniki nie idą w parze, zanim stwierdziłem, że to oczywiste, a nie po. Podałem kontekst, a następnie powiedziałem, że konkluzja z tego kontekstu jest oczywista. To nie pasuje do twojej formy.
Alice,

Autor tej odpowiedzi powiedział, że coś było oczywiste po wykonaniu ciężkiego podnoszenia. Nie sądzę, by autor chciał powiedzieć, że rozwiązanie było oczywiste.
Luke Gehorsam

10

Jak wygląda odpowiednik, który akceptuje tylko typy T pochodzące z typu List

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};

8

Streszczenie: nie rób tego.

Odpowiedź j_random_hackera mówi, jak to zrobić. Chciałbym jednak również zwrócić uwagę, że nie należy tego robić. Cały sens szablonów polega na tym, że mogą akceptować dowolny zgodny typ, a ograniczenia typu stylu Java to przerywają.

Ograniczenia typu Java to błąd, a nie funkcja. Są tam, ponieważ Java wymazuje typy typów ogólnych, więc Java nie może dowiedzieć się, jak wywołać metody na podstawie samych wartości parametrów typu.

Z drugiej strony C ++ nie ma takiego ograniczenia. Typy parametrów szablonów mogą być dowolnym typem zgodnym z operacjami, z którymi są używane. Nie musi istnieć wspólna klasa bazowa. Jest to podobne do "Duck Typing" w Pythonie, ale wykonywane w czasie kompilacji.

Prosty przykład pokazujący moc szablonów:

// Sum a vector of some type.
// Example:
// int total = sum({1,2,3,4,5});
template <typename T>
T sum(const vector<T>& vec) {
    T total = T();
    for (const T& x : vec) {
        total += x;
    }
    return total;
}

Ta funkcja sumująca może sumować wektor dowolnego typu, który obsługuje prawidłowe operacje. Działa zarówno z prymitywami, takimi jak int / long / float / double, jak i typami liczbowymi zdefiniowanymi przez użytkownika, które przeciążają operator + =. Do licha, możesz nawet użyć tej funkcji do łączenia łańcuchów, ponieważ obsługują one + =.

Nie jest konieczne pakowanie / rozpakowywanie prymitywów.

Zauważ, że konstruuje również nowe wystąpienia T przy użyciu T (). Jest to trywialne w C ++ przy użyciu niejawnych interfejsów, ale w rzeczywistości nie jest możliwe w Javie z ograniczeniami typu.

Chociaż szablony C ++ nie mają jawnych ograniczeń typu, nadal są bezpieczne dla typów i nie będą kompilować się z kodem, który nie obsługuje poprawnych operacji.


2
Jeśli sugerujesz, że nigdy nie specjalizujesz się w szablonach, czy możesz wyjaśnić, dlaczego jest to w języku?

1
Rozumiem, ale jeśli argument szablonu musi pochodzić z określonego typu, lepiej mieć łatwy do zinterpretowania komunikat z static_assert niż normalny błąd kompilatora vomit.
jhoffman0x

1
Tak, C ++ jest tutaj bardziej wyrazisty, ale chociaż jest to ogólnie dobra rzecz (ponieważ możemy wyrazić więcej za pomocą mniejszej ilości), czasami chcemy celowo ograniczyć moc, którą sobie dajemy, aby uzyskać pewność, że w pełni rozumiemy system.
j_random_hacker

Specjalizacja @Curg jest przydatna, gdy chcesz mieć możliwość skorzystania z czegoś, co można zrobić tylko dla niektórych typów. na przykład, wartość logiczna ma ~ zwykle ~ jeden bajt każdy, nawet jeśli jeden bajt może ~ normalnie ~ pomieścić 8 bitów / wartości logicznych; klasa kolekcji szablonów może (i w przypadku std :: map) specjalizować się w logicznych, więc może bardziej upakować dane, aby oszczędzać pamięć.
thecoshman

Ponadto, aby wyjaśnić, ta odpowiedź nie brzmi „nigdy nie specjalizuj szablonów”, ale mówi, że nie używaj tej funkcji do ograniczania typów, które mogą być używane z szablonem.
thecoshman

6

Nie jest to możliwe w zwykłym C ++, ale możesz zweryfikować parametry szablonu w czasie kompilacji za pomocą funkcji Concept Checking, np. Używając BCCL firmy Boost .

Od wersji C ++ 20 koncepcje stają się oficjalną cechą języka.


2
Cóż, jest to możliwe, ale sprawdzanie koncepcji jest nadal dobrym pomysłem. :)
j_random_hacker

Właściwie chodziło mi o to, że nie jest to możliwe w „zwykłym” C ++. ;)
macbirdie

5
class Base
{
    struct FooSecurity{};
};

template<class Type>
class Foo
{
    typename Type::FooSecurity If_You_Are_Reading_This_You_Tried_To_Create_An_Instance_Of_Foo_For_An_Invalid_Type;
};

Upewnij się, że klasy pochodne dziedziczą strukturę FooSecurity, a kompilator będzie zdenerwowany we wszystkich właściwych miejscach.


@Zehelvion Type::FooSecurityjest używany w klasie szablonu. Jeśli klasa, przekazana w argumencie szablonu, nie ma FooSecurity, próba jej użycia powoduje błąd. Jest pewne, że jeśli klasa przekazana w szablonie nie ma FooSecurity, z której nie pochodzi Base.
GingerPlusPlus

2

Użycie koncepcji C ++ 20

https://en.cppreference.com/w/cpp/language/constraints cppreference podaje przypadek użycia dziedziczenia jako wyraźny przykład koncepcji:

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T);  // T is constrained by Derived<T, Base>

Zgaduję, że dla wielu baz składnia będzie wyglądać następująco:

template <class T, class U, class V>
concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
 
template<Derived<Base1, Base2> T>
void f(T);

Wydaje się, że GCC 10 zaimplementowało to: https://gcc.gnu.org/gcc-10/changes.html i możesz go pobrać jako PPA na Ubuntu 20.04 . https://godbolt.org/ Mój lokalny GCC 10.1 jeszcze nie rozpoznał concept, więc nie jestem pewien, co się dzieje.


1

Czy istnieje jakiś prosty odpowiednik tego słowa kluczowego w C ++?

Nie.

W zależności od tego, co próbujesz osiągnąć, mogą istnieć odpowiednie (lub nawet lepsze) substytuty.

Przejrzałem trochę kodu STL (w Linuksie myślę, że to ten pochodzący z implementacji SGI). Ma „twierdzenia dotyczące koncepcji”; na przykład, jeśli potrzebujesz typu, który rozumie *xi ++x, twierdzenie koncepcyjne zawierałoby ten kod w funkcji nic nie rób (lub coś podobnego). Wymaga to trochę narzutów, więc mądrze może być umieszczenie go w makrze, od którego zależy definicja #ifdef debug.

Jeśli naprawdę chcesz wiedzieć o relacji podklasy, możesz stwierdzić w konstruktorze, że T instanceof list(z wyjątkiem tego, że w C ++ jest inaczej pisane). W ten sposób możesz przetestować wyjście z kompilatora, nie będąc w stanie go sprawdzić.


1

Nie ma słowa kluczowego dla tego typu kontroli, ale możesz umieścić w nim kod, który przynajmniej zawiedzie w uporządkowany sposób:

(1) Jeśli chcesz, aby szablon funkcji akceptował tylko parametry określonej klasy bazowej X, przypisz go do odwołania X w Twojej funkcji. (2) Jeśli chcesz akceptować funkcje, ale nie prymitywy lub odwrotnie, lub chcesz filtrować klasy w inny sposób, wywołaj (pustą) funkcję pomocniczą szablonu w swojej funkcji, która jest zdefiniowana tylko dla klas, które chcesz zaakceptować.

Możesz użyć (1) i (2) również w funkcjach składowych klasy, aby wymusić te sprawdzenia typu na całej klasie.

Prawdopodobnie możesz umieścić to w jakimś inteligentnym Makro, aby złagodzić ból. :)


-2

Cóż, możesz stworzyć swój szablon, czytając coś takiego:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

To jednak spowoduje, że ograniczenie będzie niejawne, a ponadto nie możesz po prostu podać niczego, co wygląda jak lista. Istnieją inne sposoby ograniczenia używanych typów kontenerów, na przykład poprzez wykorzystanie określonych typów iteratorów, które nie istnieją we wszystkich kontenerach, ale znowu jest to bardziej niejawne niż jawne ograniczenie.

O ile mi wiadomo, konstrukcja, która odzwierciedlałaby instrukcję Java w pełnym zakresie, nie istnieje w obecnym standardzie.

Istnieją sposoby na ograniczenie typów, których możesz używać w szablonie, który piszesz, za pomocą określonych czcionek typu w szablonie. Zapewni to, że kompilacja specjalizacji szablonu dla typu, który nie zawiera tej konkretnej definicji typu, nie powiedzie się, więc można selektywnie obsługiwać / nie obsługiwać niektórych typów.

W C ++ 11 wprowadzenie pojęć powinno to ułatwić, ale nie sądzę, że zrobi to dokładnie to, czego byś chciał.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.