Come funziona `void_t`


149

Ho visto il discorso di Walter Brown a Cppcon14 sulla programmazione di modelli moderni ( Parte I , Parte II ) in cui ha presentato la sua void_ttecnica SFINAE.

Esempio:
dato un semplice modello variabile che valuta voidse tutti gli argomenti del modello sono ben formati:

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

e la seguente caratteristica che verifica l'esistenza di una variabile membro denominata membro :

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

Ho cercato di capire perché e come funziona. Pertanto un piccolo esempio:

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 esiste
    • decltype( A::member ) è ben formato
    • void_t<> è valido e valuta void
  • has_member< A , void > e quindi sceglie il modello specializzato
  • has_member< T , void > e valuta true_type

2. has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member non esiste
    • decltype( B::member ) è mal formato e fallisce silenziosamente (sfinae)
    • has_member< B , expression-sfinae > quindi questo modello viene scartato
  • il compilatore trova has_member< B , class = void >void come argomento predefinito
  • has_member< B > valuta false_type

http://ideone.com/HCTlBb

Domande:
1. La mia comprensione di questo è corretta?
2. Walter Brown afferma che l'argomento predefinito deve essere esattamente lo stesso tipo di quello usato void_tperché funzioni. Perché? (Non vedo perché questi tipi debbano corrispondere, non funziona qualsiasi tipo predefinito?)


6
Ad 2) Immaginate l'asserzione statico è stato scritto come: has_member<A,int>::value. Quindi, la specializzazione parziale che valuta has_member<A,void>non può corrispondere. Pertanto, deve essere has_member<A,void>::value, o, con zucchero sintattico, un argomento predefinito di tipo void.
dyp,

1
@dyp Grazie, lo modificherò. Mh, non vedo ancora la necessità di avere has_member< T , class = void >inadempienze void. Supponendo che questa caratteristica verrà utilizzata solo con 1 argomento template in qualsiasi momento, allora l'argomento predefinito potrebbe essere di qualsiasi tipo?
risarcimento il

Domanda interessante.
Dopo il

2
Nota che in questa proposta, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Walter è cambiato template <class, class = void>in template <class, class = void_t<>>. Quindi ora siamo liberi di fare tutto ciò che vogliamo con l' void_timplementazione del modello alias :)
JohnKoch

Risposte:


133

1. Modello di classe primaria

Quando scrivi has_member<A>::value, il compilatore cerca il nome has_membere trova il modello di classe principale , ovvero questa dichiarazione:

template< class , class = void >
struct has_member;

(Nel PO, è scritto come una definizione.)

L'elenco degli argomenti del modello <A>viene confrontato con l'elenco dei parametri del modello di questo modello principale. Dal momento che il modello primario ha due parametri, ma è fornito solo uno, il parametro rimanente viene default all'argomento modello predefinito: void. È come se avessi scritto has_member<A, void>::value.

2. Modello di classe specializzato

Ora , l'elenco dei parametri del modello viene confrontato con qualsiasi specializzazione del modello has_member. Solo se nessuna specializzazione corrisponde, la definizione del modello primario viene utilizzata come fallback. Quindi la specializzazione parziale viene presa in considerazione:

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

Il compilatore tenta di abbinare gli argomenti del modello A, voidcon i modelli definiti nella specializzazione parziale: Te void_t<..>uno per uno. Innanzitutto , viene eseguita la deduzione dell'argomento modello. La specializzazione parziale sopra è ancora un modello con parametri modello che devono essere "riempiti" da argomenti.

Il primo modello T consente al compilatore di dedurre il parametro template T. Questa è una deduzione banale, ma considera un modello come T const&, dove potremmo ancora dedurlo T. Per lo schema Te l'argomento modello A, deduciamo Tessere A.

Nel secondo modello void_t< decltype( T::member ) > , il parametro template Tappare in un contesto in cui non può essere dedotto da nessun argomento template.

Ci sono due ragioni per questo:

  • L'espressione all'interno decltypeè esplicitamente esclusa dalla deduzione dell'argomento template. Immagino che ciò sia dovuto al fatto che può essere arbitrariamente complesso.

  • Anche se abbiamo usato un modello senza decltypelike void_t< T >, la deduzione di Tavviene sul modello di alias risolto. Cioè, risolviamo il modello di alias e successivamente proviamo a dedurre il tipo Tdal modello risultante. Il modello risultante, tuttavia, è void, che non dipende da Te quindi non ci consente di trovare un tipo specifico per T. Questo è simile al problema matematico del tentativo di invertire una funzione costante (nel senso matematico di quei termini).

Detrazione argomento di un template è finito (*) , ora le dedotte argomenti template vengono sostituiti. Questo crea una specializzazione simile a questa:

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

Il tipo void_t< decltype( A::member ) >può ora essere valutato. È ben formato dopo la sostituzione, quindi non si verifica alcun errore di sostituzione . Noi abbiamo:

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

3. Scelta

Ora , possiamo confrontare l'elenco dei parametri del modello di questa specializzazione con gli argomenti del modello forniti all'originale has_member<A>::value. Entrambi i tipi corrispondono esattamente, quindi viene scelta questa specializzazione parziale.


D'altra parte, quando definiamo il modello come:

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

Finiamo con la stessa specializzazione:

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

ma il nostro elenco di argomenti modello per has_member<A>::valueora lo è <A, int>. Gli argomenti non corrispondono ai parametri della specializzazione e il modello principale viene scelto come fallback.


(*) Lo Standard, IMHO in modo confuso, include il processo di sostituzione e la corrispondenza degli argomenti del modello esplicitamente specificati nel processo di deduzione degli argomenti del modello . Ad esempio (post-N4296) [temp.class.spec.match] / 2:

Una specializzazione parziale corrisponde a un determinato elenco di argomenti del modello effettivo se gli argomenti del modello della specializzazione parziale possono essere dedotti dall'elenco degli argomenti del modello effettivo.

Ma ciò non significa solo che tutti i parametri modello della specializzazione parziale debbano essere dedotti; significa anche che la sostituzione deve avere esito positivo e (come sembra?) gli argomenti del modello devono corrispondere ai parametri (sostituiti) del modello della specializzazione parziale. Si noti che non sono completamente consapevole di dove lo standard specifica il confronto tra l'elenco degli argomenti sostituiti e l'elenco degli argomenti fornito.


3
Grazie! L'ho letto più e più volte, e immagino che il mio modo di pensare a come funzioni esattamente la deduzione dell'argomento template e che cosa il compilatore sceglie per il template finale non sia al momento corretto.
assenso il

1
@ JohannesSchaub-litb Grazie! È un po 'deprimente, però. Non ci sono davvero regole per abbinare un argomento template con una specializzazione? Neanche per specializzazioni esplicite?
dyp,

2
Argomenti modello predefinito W / r / t, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC

1
@dyp Alcune settimane dopo, leggendo molto su questo e con un suggerimento da questo frammento, penso di iniziare a capire come funziona. La tua spiegazione da leggere a leggere ha più senso per me, grazie!
assenso il

1
Volevo aggiungere che il termine template principale era la chiave (i template si incontrano per la prima volta nel codice)
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
{ };

Questa specializzazione sopra esiste solo quando è ben formata, quindi quando decltype( T::member )è valida e non ambigua. la specializzazione è così has_member<T , void>come stato nel commento.

Quando scrivi has_member<A>, è a has_member<A, void>causa dell'argomento template predefinito.

E abbiamo una specializzazione per has_member<A, void>(quindi eredita da true_type) ma non abbiamo una specializzazione per has_member<B, void>(quindi usiamo la definizione predefinita: eredita da false_type)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.