Como funciona o `void_t`

Eu assisti a palestra de Walter Brown na Cppcon14 sobre programação de modelos modernos ( Parte I , Parte II ) onde ele apresentou sua técnica void_t SFINAE.

Exemplo:
Dado um modelo de variável simples que avalia como void se todos os argumentos de modelo são bem formados:

 template using void_t = void; 

e o seguinte traço que verifica a existência de uma variável de membro chamada 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 { }; 

Eu tentei entender por que e como isso funciona. Portanto, um pequeno exemplo:

 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 não existe
    • decltype( B::member ) está mal formado e falha silenciosamente (sfinae)
    • has_member então este modelo é descartado
  • compilador encontra has_member com void como argumento padrão
  • has_member avaliado como false_type

http://ideone.com/HCTlBb

Questões:
1. Meu entendimento disso é correto?
2. Walter Brown afirma que o argumento padrão tem que ser exatamente o mesmo tipo que o usado em void_t para funcionar. Por que é que? (Não vejo por que esses tipos precisam corresponder, não é qualquer tipo padrão que faz o trabalho?)

Quando você escreve has_member::value , o compilador pesquisa o nome has_member e localiza o modelo de class primário , ou seja, esta declaração:

 template< class , class = void > struct has_member; 

(No OP, isso é escrito como uma definição.)

A lista de argumentos do modelo é comparada à lista de parâmetros do modelo desse modelo principal. Como o modelo principal tem dois parâmetros, mas você apenas forneceu um, o parâmetro restante é padronizado para o argumento do modelo padrão: void . É como se você tivesse escrito has_member::value .

Agora, a lista de parâmetros do modelo é comparada com qualquer especialização do modelo has_member . Somente se nenhuma especialização corresponder, a definição do modelo principal será usada como fallback. Então, a especialização parcial é levada em conta:

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

O compilador tenta corresponder aos argumentos do modelo A, void com os padrões definidos na especialização parcial: T e void_t< ..> um por um. Primeiro, dedução de argumento de modelo é executada. A especialização parcial acima ainda é um modelo com parâmetros de modelo que precisam ser “preenchidos” por argumentos.

O primeiro padrão, T , permite ao compilador deduzir o parâmetro-modelo T Esta é uma dedução trivial, mas considere um padrão como T const& , onde ainda podemos deduzir T Para o padrão T e o argumento modelo A , deduzimos que T seja A

No segundo padrão void_t< decltype( T::member ) > , o parâmetro de modelo T aparece em um contexto onde não pode ser deduzido de qualquer argumento de template. Há duas razões para isso:

  • A expressão dentro de decltype é explicitamente excluída da dedução de argumento de modelo. Eu acho que isso é porque pode ser arbitrariamente complexo.

  • Mesmo se usarmos um padrão sem decltype como void_t< T > , a dedução de T acontece no modelo de alias resolvido. Ou seja, resolvemos o modelo de alias e tentamos deduzir o tipo T do padrão resultante. O padrão resultante, no entanto, é void , o qual não é dependente de T e, portanto, não nos permite encontrar um tipo específico para T Isso é semelhante ao problema matemático de tentar inverter uma function constante (no sentido matemático desses termos).

A dedução de argumento de modelo está concluída (*) , agora os argumentos de modelo deduzidos são substituídos. Isso cria uma especialização que se parece com isso:

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

O tipo void_t< decltype( A::member ) > > agora pode ser avaliado. É bem formado após a substituição, portanto, não ocorre nenhuma falha de substituição . Nós temos:

 template<> struct has_member : true_type { }; 

Agora, podemos comparar a lista de parâmetros de modelo desta especialização com os argumentos de modelo fornecidos para o has_member::value . Ambos os tipos correspondem exatamente, então essa especialização parcial é escolhida.

Por outro lado, quando definimos o modelo como:

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

Acabamos com a mesma especialização:

 template<> struct has_member : true_type { }; 

mas nossa lista de argumentos de modelo para o has_member::value agora é . Os argumentos não correspondem aos parâmetros da especialização e o modelo principal é escolhido como um fall-back.


(*) O padrão, IMHO confusamente, inclui o processo de substituição e a correspondência de argumentos de modelo explicitamente especificados no processo de dedução de argumento de modelo . Por exemplo (post-N4296) [temp.class.spec.match] / 2:

Uma especialização parcial corresponde a uma determinada lista de argumentos de modelo real se os argumentos de modelo da especialização parcial puderem ser deduzidos da lista de argumentos de modelo reais.

Mas isso não significa apenas que todos os parâmetros-modelo da especialização parcial devem ser deduzidos; isso também significa que a substituição deve ser bem-sucedida e (como parece) os argumentos de modelo devem corresponder aos parâmetros de modelo (substituídos) da especialização parcial. Observe que não estou completamente ciente de onde o Padrão especifica a comparação entre a lista de argumentos substituídos e a lista de argumentos fornecida.

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

Essa especialização acima existe somente quando está bem formada, portanto, quando decltype( T::member ) é válido e não é ambíguo. a especialização é assim para has_member como estado no comentário.

Quando você escreve has_member , é has_member devido ao argumento do modelo padrão.

E temos especialização para has_member (portanto, herde de true_type ) mas não temos especialização para has_member (então usamos a definição padrão: inherit from false_type )