Come funziona `void_t`

Ho visto il discorso di Walter Brown su Cppcon14 sulla programmazione dei template moderni ( Parte I , Parte II ) in cui ha presentato la sua tecnica void_t SFINAE.

Esempio:
Dato un modello di variabile semplice che void se tutti gli argomenti del modello sono ben formati:

 template using void_t = void; 

e la seguente caratteristica che controlla l’esistenza di una variabile membro chiamata membro :

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

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

 class A { public: int member; }; class B { }; static_assert( has_member::value , "A" ); static_assert( has_member::value , "B" ); 

1. has_member

2. has_member

  • has_member< B , void_t >
    • B::member non esiste
    • decltype( B::member ) è mal formato e fallisce silenziosamente (sfinae)
    • has_member così questo modello viene scartato
  • il compilatore trova has_member con void come argomento predefinito
  • has_member valuta su 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 di quello usato in void_t perché funzioni. Perché? (Non vedo perché questo tipo debba corrispondere, non è proprio il tipo predefinito a fare il lavoro?)

Quando scrivi has_member::value , il compilatore cerca il nome has_member e trova il modello di class primaria , cioè questa dichiarazione:

 template< class , class = void > struct has_member; 

(Nell’OP, è scritto come una definizione).

L’elenco degli argomenti del modello viene confrontato con l’elenco dei parametri del modello di questo modello primario. Poiché il modello principale ha due parametri, ma ne è stato fornito uno solo, il parametro rimanente viene impostato come predefinito sull’argomento modello predefinito: void . È come se avessi scritto has_member::value .

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

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

Il compilatore tenta di far corrispondere gli argomenti del modello A, void con i modelli definiti nella specializzazione parziale: T e void_t<..> uno per uno. Innanzitutto, viene eseguita la deduzione degli argomenti del modello. La specializzazione parziale di cui sopra è ancora un modello con parametri modello che devono essere “riempiti” dagli argomenti.

Il primo modello, T , consente al compilatore di dedurre il parametro modello T Questa è una deduzione banale, ma considera un modello come T const& , dove potremmo ancora dedurre T Per il modello T e l’argomento del modello A , deduciamo che T è A

Nel secondo modello void_t< decltype( T::member ) > , il parametro template T appare in un contesto in cui non può essere dedotto da alcun argomento del template. Ci sono due ragioni per questo:

  • L’espressione all’interno di decltype è esplicitamente esclusa dalla deduzione degli argomenti del modello. Immagino che questo sia perché può essere arbitrariamente complesso.

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

La deduzione degli argomenti del template è terminata (*) , ora gli argomenti del template dedotti sono 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 alcuna sostituzione . Noi abbiamo:

 template<> struct has_member : true_type { }; 

Ora, possiamo confrontare l’elenco dei parametri del modello di questa specializzazione con gli argomenti del modello forniti al has_member::value originale. 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 : true_type { }; 

ma la nostra lista di argomenti template per has_member::value ora è . Gli argomenti non corrispondono ai parametri della specializzazione e il modello primario viene scelto come fallback.


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

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

Ma questo non significa solo che tutti i parametri del modello della specializzazione parziale devono essere dedotti; significa anche che la sostituzione deve avere successo e (come sembra?) gli argomenti del modello devono corrispondere ai parametri del modello (sostituito) della specializzazione parziale. Si noti che non sono completamente consapevole di dove lo Standard specifica il confronto tra la lista degli argomenti sostituiti e l’elenco degli argomenti forniti.

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

Quella specializzazione sopra esiste solo quando è ben formata, quindi quando decltype( T::member ) è valido e non ambiguo. la specializzazione è così per has_member come stato nel commento.

Quando scrivi has_member , è has_member causa dell’argomento template predefinito.

E abbiamo specializzazione per has_member (quindi ereditato da true_type ) ma non abbiamo specializzazione per has_member (quindi usiamo la definizione predefinita: inherit from false_type )