Wie funktioniert `void_t`?

148

Ich habe Walter Browns Vortrag auf der Cppcon14 über moderne Vorlagenprogrammierung ( Teil I , Teil II ) gesehen, in dem er seine void_tSFINAE-Technik vorstellte.

Beispiel:
Bei einer einfachen Variablenvorlage, die bewertet, voidob alle Vorlagenargumente gut geformt sind:

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

und das folgende Merkmal, das die Existenz einer Mitgliedsvariablen namens member prüft :

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
{ };

Ich habe versucht zu verstehen, warum und wie das funktioniert. Daher ein kleines Beispiel:

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 existiert
    • decltype( A::member ) ist wohlgeformt
    • void_t<> ist gültig und wertet aus void
  • has_member< A , void > und deshalb wählt es die spezielle Vorlage
  • has_member< T , void > und bewertet zu true_type

2. has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member ist nicht vorhanden
    • decltype( B::member ) ist schlecht geformt und versagt lautlos (sfinae)
    • has_member< B , expression-sfinae > Daher wird diese Vorlage verworfen
  • Compiler findet has_member< B , class = void >mit void als Standardargument
  • has_member< B > bewertet zu false_type

http://ideone.com/HCTlBb

Fragen:
1. Ist mein Verständnis davon richtig?
2. Walter Brown gibt an, dass das Standardargument genau der Typ sein muss, in dem es verwendet wird, void_tdamit es funktioniert. Warum ist das so? (Ich verstehe nicht, warum diese Typen übereinstimmen müssen, erledigt nicht irgendein Standardtyp den Job?)

Nichtsensation
quelle
6
Anzeige 2) Stellen Sie sich vor, die statische Behauptung wurde wie folgt geschrieben : has_member<A,int>::value. Dann kann die Teilspezialisierung, die ausgewertet wird, has_member<A,void>nicht übereinstimmen. Daher muss es has_member<A,void>::valueoder mit syntaktischem Zucker ein Standardargument vom Typ sein void.
Dyp
1
@dyp Danke, das werde ich bearbeiten. Mh, ich sehe keine Notwendigkeit darin, noch in has_member< T , class = void >Verzug zu sein void. Angenommen, dieses Merkmal wird zu jedem Zeitpunkt nur mit 1 Vorlagenargument verwendet, dann könnte das Standardargument ein beliebiger Typ sein?
Nonsensation
Interessante Frage.
AStopher
2
Beachten Sie, dass in diesem Vorschlag, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Walter geändert template <class, class = void>zu template <class, class = void_t<>>. Jetzt können wir mit der void_tImplementierung der Alias-Vorlage tun, was wir wollen :)
JohnKoch

Antworten:

132

1. Vorlage für die Primärklasse

Wenn Sie schreiben has_member<A>::value, sucht der Compiler nach dem Namen has_memberund findet die primäre Klassenvorlage, dh diese Deklaration:

template< class , class = void >
struct has_member;

(Im OP ist das als Definition geschrieben.)

Die Liste <A>der Vorlagenargumente wird mit der Liste der Vorlagenparameter dieser primären Vorlage verglichen. Da die primäre Vorlage zwei Parameter enthält, Sie jedoch nur einen angegeben haben, wird für den verbleibenden Parameter standardmäßig das Standardvorlagenargument verwendet : void. Es ist, als hättest du geschrieben has_member<A, void>::value.

2. Spezialisierte Klassenvorlage

Jetzt wird die Vorlagenparameterliste mit allen Spezialisierungen der Vorlage verglichen has_member. Nur wenn keine Spezialisierung übereinstimmt, wird die Definition der primären Vorlage als Ersatz verwendet. Die Teilspezialisierung wird also berücksichtigt:

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

Der Compiler versucht, die Vorlagenargumente A, voidmit den in der Teilspezialisierung definierten Mustern abzugleichen: Tund zwar void_t<..>nacheinander. Zunächst wird der Abzug der Vorlagenargumente durchgeführt. Die obige Teilspezialisierung ist immer noch eine Vorlage mit Vorlagenparametern, die mit Argumenten "gefüllt" werden müssen.

Das erste Muster T ermöglicht es dem Compiler, den Template-Parameter abzuleiten T. Dies ist ein trivialer Abzug, aber betrachten Sie ein Muster wie T const&, aus dem wir noch ableiten könnten T. Für das Muster Tund das Template - Argument A, folgern wir Tsein A.

Im zweiten Muster void_t< decltype( T::member ) > erscheint der Template-Parameter Tin einem Kontext, in dem er aus keinem Template-Argument abgeleitet werden kann.

Dafür gibt es zwei Gründe:

  • Der Ausdruck im Inneren decltypewird ausdrücklich vom Abzug von Vorlagenargumenten ausgeschlossen. Ich denke, das liegt daran, dass es beliebig komplex sein kann.

  • Auch wenn verwenden wir ein Muster ohne decltypewie void_t< T >, dann Abzug Tgeschieht auf der Alias - Vorlage aufgelöst. Das heißt, wir lösen die Alias-Vorlage auf und versuchen später, den Typ Taus dem resultierenden Muster abzuleiten . Das resultierende Muster ist jedoch, voiddas nicht abhängig ist Tund es uns daher nicht erlaubt, einen bestimmten Typ für zu finden T. Dies ähnelt dem mathematischen Problem des Versuchs, eine konstante Funktion zu invertieren (im mathematischen Sinne dieser Begriffe).

Der Abzug der Vorlagenargumente ist abgeschlossen (*) , jetzt werden die abgeleiteten Vorlagenargumente ersetzt. Dadurch entsteht eine Spezialisierung, die folgendermaßen aussieht:

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

Der Typ void_t< decltype( A::member ) >kann jetzt ausgewertet werden. Es ist nach der Substitution gut ausgebildet, daher tritt kein Substitutionsfehler auf. Wir bekommen:

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

3. Wahl

Jetzt können wir die Vorlagenparameterliste dieser Spezialisierung mit den Vorlagenargumenten vergleichen, die dem Original zur Verfügung gestellt wurden has_member<A>::value. Beide Typen stimmen genau überein, daher wird diese Teilspezialisierung gewählt.


Auf der anderen Seite, wenn wir die Vorlage definieren als:

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
{ };

Wir haben die gleiche Spezialisierung:

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

aber unsere Vorlagenargumentliste für has_member<A>::valuejetzt ist <A, int>. Die Argumente stimmen nicht mit den Parametern der Spezialisierung überein, und die primäre Vorlage wird als Ersatz ausgewählt.


(*) Der Standard enthält meiner Meinung nach verwirrenderweise den Ersetzungsprozess und den Abgleich explizit angegebener Vorlagenargumente in den Vorlagenargument-Abzugsprozess . Zum Beispiel (nach N4296) [temp.class.spec.match] / 2:

Eine Teilspezialisierung entspricht einer bestimmten Liste der tatsächlichen Vorlagenargumente, wenn die Vorlagenargumente der Teilspezialisierung aus der Liste der tatsächlichen Vorlagenargumente abgeleitet werden können.

Dies bedeutet aber nicht nur , dass alle Template-Parameter der Teilspezialisierung abgeleitet werden müssen; Dies bedeutet auch, dass die Substitution erfolgreich sein muss und (wie es scheint?) die Vorlagenargumente mit den (substituierten) Vorlagenparametern der Teilspezialisierung übereinstimmen müssen. Beachten Sie, dass mir nicht vollständig bekannt ist, wo der Standard den Vergleich zwischen der Liste der ersetzten Argumente und der Liste der bereitgestellten Argumente angibt.

dyp
quelle
3
Danke dir! Ich habe es immer und immer wieder gelesen und ich denke, meine Überlegungen, wie die Ableitung von Vorlagenargumenten genau funktioniert und was der Compiler für die endgültige Vorlage auswählt, sind im Moment nicht korrekt.
Nonsensation
1
@ JohannesSchaub-litb Danke! Das ist allerdings etwas deprimierend. Gibt es wirklich keine Regeln, um ein Vorlagenargument mit einer Spezialisierung abzugleichen? Nicht einmal für explizite Spezialisierungen?
Dyp
2
W / r / t Standardvorlagenargumente, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC
1
@dyp Ein paar Wochen später und wenn ich viel darüber lese und mit einem Hinweis aus diesem Ausschnitt denke ich, dass ich anfange zu verstehen, wie das funktioniert. Ihre Erklärung macht für mich von Lesen zu Lesen mehr Sinn, danke!
Nonsensation
1
Ich wollte hinzufügen, dass der Begriff primäre Vorlage der Schlüssel war (die Vorlagen erste Begegnung im Code)
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
{ };

Diese obige Spezialisierung existiert nur, wenn sie gut geformt ist, also wenn sie decltype( T::member )gültig und nicht mehrdeutig ist. Die Spezialisierung ist so has_member<T , void>wie im Kommentar angegeben.

Wenn Sie schreiben has_member<A>, liegt dies has_member<A, void>an dem Standardvorlagenargument.

Und wir haben Spezialisierung für has_member<A, void>(also erben von true_type), aber wir haben keine Spezialisierung für has_member<B, void>(also verwenden wir die Standarddefinition: erben von false_type)

Jarod42
quelle