Jak działa „void_t”

149

Oglądałem wykład Waltera Browna na Cppcon14 o nowoczesnym programowaniu szablonów ( część I , część II ), gdzie przedstawił swoją void_ttechnikę SFINAE.

Przykład:
biorąc pod uwagę prosty szablon zmiennej, który ocenia, voidczy wszystkie argumenty szablonu są poprawnie sformułowane:

template< class ... > using void_t = void;

oraz następującą cechę, która sprawdza istnienie zmiennej składowej o nazwie member :

template< class , class = void >
struct has_member : std::false_type
{ };

// specialized as has_member< T , void > or discarded (sfinae)
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : std::true_type
{ };

Próbowałem zrozumieć, dlaczego i jak to działa. Dlatego mały przykład:

class A {
public:
    int member;
};

class B {
};

static_assert( has_member< A >::value , "A" );
static_assert( has_member< B >::value , "B" );

1. has_member< A >

  • has_member< A , void_t< decltype( A::member ) > >
    • A::member istnieje
    • decltype( A::member ) jest dobrze uformowany
    • void_t<> jest ważny i jest oceniany do void
  • has_member< A , void > i dlatego wybiera specjalistyczny szablon
  • has_member< T , void > i ocenia do true_type

2. has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member nie istnieje
    • decltype( B::member ) jest źle ukształtowany i cicho zawodzi (sfinae)
    • has_member< B , expression-sfinae > więc ten szablon jest odrzucany
  • kompilator znajdzie has_member< B , class = void >argument void jako domyślny
  • has_member< B > ocenia do false_type

http://ideone.com/HCTlBb

Pytania:
1. Czy moje rozumienie tego jest prawidłowe?
2. Walter Brown stwierdza, że ​​domyślny argument musi być dokładnie tego samego typu, co używany w programie void_t, aby działał. Dlaczego? (Nie rozumiem, dlaczego te typy muszą pasować, czy żaden domyślny typ nie spełnia tego zadania?)

bez sensu
źródło
6
Ad 2) Wyobraź sobie assert statyczny został zapisany jako: has_member<A,int>::value. Wtedy częściowa specjalizacja, której has_member<A,void>wynikiem jest ocena, nie może być zgodna. Dlatego musi to być has_member<A,void>::valuelub, w przypadku cukru składniowego, domyślny argument typu void.
dyp
1
@dyp Dzięki, zmienię to. Mh, nie widzę jeszcze potrzeby has_member< T , class = void >wpadania w zwłokę void. Zakładając, że ta cecha będzie używana tylko z 1 argumentem szablonu w dowolnym momencie, to domyślny argument może być dowolnego typu?
nonsensation
Interesujące pytanie.
AStopher
2
Zwróć uwagę, że w tej propozycji, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Walter zmienił się template <class, class = void>na template <class, class = void_t<>>. Więc teraz możemy robić, co chcemy, z void_timplementacją szablonu aliasu :)
JohnKoch,

Odpowiedzi:

133

1. Szablon klasy podstawowej

Podczas pisania has_member<A>::valuekompilator wyszukuje nazwę has_memberi znajduje podstawowy szablon klasy, czyli taką deklarację:

template< class , class = void >
struct has_member;

(W OP jest to zapisane jako definicja).

Lista argumentów szablonu <A>jest porównywana z listą parametrów szablonu tego szablonu podstawowego. Ponieważ podstawowy szablon ma dwa parametry, ale tylko w zestawie jeden, pozostałe parametr jest domyślnie domyślnego szablonu argumentu: void. To tak, jakbyś napisał has_member<A, void>::value.

2. Specjalistyczny szablon klasy

Teraz lista parametrów szablonu jest porównywana z dowolnymi specjalizacjami szablonu has_member. Tylko wtedy, gdy żadna specjalizacja nie pasuje, definicja szablonu podstawowego jest używana jako rozwiązanie rezerwowe. Tak więc brana jest pod uwagę specjalizacja częściowa:

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

Kompilator próbuje dopasować argumenty szablonu A, voiddo wzorców zdefiniowanych w częściowej specjalizacji: Ti void_t<..>jeden po drugim. Najpierw wykonywana jest dedukcja argumentów szablonu. Powyższa częściowa specjalizacja to nadal szablon z parametrami szablonu, które muszą być „wypełnione” argumentami.

Pierwszy wzorzec T pozwala kompilatorowi wydedukować parametr szablonu T. Jest to trywialna dedukcja, ale rozważ wzorzec T const&, w którym nadal moglibyśmy wywnioskować T. Dla wzorca Ti argumentu szablonu wnioskujemy A, że Tjest A.

W drugim wzorcu void_t< decltype( T::member ) > parametr-szablon Tpojawia się w kontekście, w którym nie można go wywnioskować z żadnego argumentu szablonu.

Są ku temu dwa powody:

  • Wyrażenie wewnątrz decltypejest jawnie wykluczone z dedukcji argumentów szablonu. Wydaje mi się, że dzieje się tak, ponieważ może to być dowolnie złożone.

  • Nawet jeśli użyliśmy wzorca bez decltypepolubienia void_t< T >, to potrącenie Tnastępuje na rozwiązanym szablonie aliasu. Oznacza to, że rozwiązujemy szablon aliasu, a później próbujemy wydedukować typ Tz otrzymanego wzorca. Wynikowy wzorzec jest jednak, voidktóry nie jest zależny Ti dlatego nie pozwala nam znaleźć określonego typu T. Jest to podobne do problemu matematycznego polegającego na próbie odwrócenia funkcji stałej (w matematycznym sensie tych terminów).

Odliczenie argumentu szablon jest zakończona (*) , teraz to wywnioskowane argumenty szablonu są podstawione. Tworzy to specjalizację, która wygląda następująco:

template<>
struct has_member< A, void_t< decltype( A::member ) > > : true_type
{ };

void_t< decltype( A::member ) >Teraz można ocenić typ . Jest dobrze uformowany po zmianie, dlatego nie występuje żadna awaria zamiany . Otrzymujemy:

template<>
struct has_member<A, void> : true_type
{ };

3. Wybór

Teraz możemy porównać listę parametrów szablonu tej specjalizacji z argumentami szablonu dostarczonymi do oryginału has_member<A>::value. Oba typy pasują dokładnie, więc wybrana jest ta częściowa specjalizacja.


Z drugiej strony, gdy zdefiniujemy szablon jako:

template< class , class = int > // <-- int here instead of void
struct has_member : false_type
{ };

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

Kończymy z tą samą specjalizacją:

template<>
struct has_member<A, void> : true_type
{ };

ale na razie nasza lista argumentów szablonów has_member<A>::valueto <A, int>. Argumenty nie pasują do parametrów specjalizacji, a szablon podstawowy jest wybierany jako rezerwowy.


(*) Standard, IMHO myląco, obejmuje proces podstawiania i dopasowywanie jawnie określonych argumentów szablonu w procesie dedukcji argumentów szablonu . Na przykład (po N4296) [temp.class.spec.match] / 2:

Częściowa specjalizacja jest zgodna z daną rzeczywistą listą argumentów szablonu, jeśli argumenty szablonu częściowej specjalizacji można wywnioskować z rzeczywistej listy argumentów szablonu.

Ale to nie tylko oznacza, że ​​należy wydedukować wszystkie parametry szablonu częściowej specjalizacji; oznacza to również, że podstawienie musi się powieść i (jak się wydaje?) argumenty szablonu muszą być zgodne z (podstawionymi) parametrami szablonu częściowej specjalizacji. Zauważ, że nie jestem całkowicie świadomy tego, gdzie Standard określa porównanie między podstawioną listą argumentów a dostarczoną listą argumentów.

dyp
źródło
3
Dziękuję Ci! Czytałem to w kółko i wydaje mi się, że moje myślenie o tym, jak dokładnie działa dedukcja argumentów z szablonu i co kompilator wybiera dla ostatecznego szablonu, jest w tej chwili nieprawidłowe.
nonsensation
1
@ JohannesSchaub-litb Dzięki! To trochę przygnębiające. Czy naprawdę nie ma reguł dopasowywania argumentu szablonu ze specjalizacją? Nawet w przypadku wyraźnych specjalizacji?
dyp
2
W / r / t domyślne argumenty szablonu, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC
1
@dyp Kilka tygodni później i czytając dużo na ten temat oraz podpowiedź z tego fragmentu, myślę, że zaczynam rozumieć, jak to działa. Twoje wyjaśnienie sprawia, że ​​czytanie staje się dla mnie bardziej sensowne, dzięki!
nonsensation
1
Chciałem dodać, że kluczem był termin podstawowy szablon (pierwsze spotkanie z szablonami w kodzie)
nonsensation
18
// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };

Że powyższa specjalizacja istnieje tylko wtedy, gdy jest dobrze ukształtowana, a więc kiedy decltype( T::member )jest ważna i niejednoznaczna. specjalizacja jest taka, has_member<T , void>jak zaznaczam w komentarzu.

Kiedy piszesz has_member<A>, dzieje się has_member<A, void>tak z powodu domyślnego argumentu szablonu.

Mamy specjalizację dla has_member<A, void>(więc dziedzicz z true_type), ale nie mamy specjalizacji dla has_member<B, void>(więc używamy domyślnej definicji: dziedziczenie z false_type)

Jarod42
źródło