Como o `void_t` funciona

149

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

Exemplo:
Dado um modelo de variável simples que avalia voidse todos os argumentos do modelo estão bem formados:

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

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

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

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 existe
    • decltype( A::member ) é bem formado
    • void_t<> é válido e avalia como void
  • has_member< A , void > e, portanto, escolhe o modelo especializado
  • has_member< T , void > e avalia para true_type

2) has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member não existe
    • decltype( B::member ) está mal formado e falha silenciosamente (sfinae)
    • has_member< B , expression-sfinae > então esse modelo é descartado
  • compilador encontra has_member< B , class = void >com void como argumento padrão
  • has_member< B > avalia como false_type

http://ideone.com/HCTlBb

Perguntas:
1. O meu entendimento está correto?
2. Walter Brown afirma que o argumento padrão deve ser exatamente o mesmo tipo que o usado void_tpara que ele funcione. Por que é que? (Não vejo por que esses tipos precisam corresponder, apenas um tipo padrão não funciona?)

absurdo
fonte
6
Anúncio 2) Imagine a assert estática foi escrito como: has_member<A,int>::value. Em seguida, a especialização parcial avaliada has_member<A,void>não pode corresponder. Portanto, ele precisa ser has_member<A,void>::value, ou, com açúcar sintático, um argumento padrão do tipo void.
dyp 29/12/14
1
@ Dyp Obrigado, eu vou editar isso. Não vejo a necessidade de deixar o has_member< T , class = void >padrão voidainda. Supondo que essa característica seja usada apenas com 1 argumento de modelo a qualquer momento, o argumento padrão pode ser de qualquer tipo?
Nonsensation
Pergunta interessante.
AStopher
2
Observe que, nesta proposta, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Walter mudou template <class, class = void>para template <class, class = void_t<>>. Então agora somos livres para fazer o que quisermos com void_ta implementação do modelo pseudônimo :)
JohnKoch

Respostas:

133

1. Modelo de Classe Primária

Quando você escreve has_member<A>::value, o compilador consulta o nome has_membere encontra o modelo de classe principal , 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 <A>é comparada à lista de parâmetros do modelo deste modelo principal. Desde o modelo principal tem dois parâmetros, mas só fornecido um, o parâmetro restante é padrão para o argumento de modelo padrão: void. É como se você tivesse escrito has_member<A, void>::value.

2. Modelo de Classe Especializada

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 primário será usada como fallback. Portanto, a especialização parcial é levada em consideração:

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

O compilador tenta combinar os argumentos do modelo A, voidcom os padrões definidos na especialização parcial: Te void_t<..>um por um. Primeiro , é realizada a dedução do argumento do modelo. 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 que o compilador deduza 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 Te o argumento do modelo A, deduzimos Tser A.

No segundo padrão void_t< decltype( T::member ) > , o parâmetro do modelo Taparece em um contexto em que não pode ser deduzido de nenhum argumento do modelo.

Há duas razões para isso:

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

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

A dedução do argumento do modelo está concluída (*) , agora os argumentos do modelo deduzido 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, nenhuma falha de substituição ocorre. Nós temos:

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

3. Escolha

Agora , podemos comparar a lista de parâmetros do modelo desta especialização com os argumentos do modelo fornecidos ao original has_member<A>::value. Ambos os tipos correspondem exatamente, portanto, 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
{ };

Terminamos com a mesma especialização:

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

mas a nossa lista de argumentos de modelos por has_member<A>::valueenquanto é <A, int>. Os argumentos não correspondem aos parâmetros da especialização, e o modelo primário é escolhido como substituto.


(*) O Padrão, IMHO, confuso, 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 (pós-N4296) [temp.class.spec.match] / 2:

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

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 ter sucesso e (como parece?) os argumentos do modelo devem corresponder aos parâmetros (substituídos) do modelo 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.

dyp
fonte
3
Obrigado! Eu li várias vezes e acho que meu pensamento sobre como a dedução de argumento do modelo funciona exatamente e o que o compilador escolhe para o modelo final não está correto no momento.
Nonsensation
1
@ JohannesSchaub-litb Obrigado! Isso é um pouco deprimente, no entanto. Realmente não existem regras para combinar um argumento de modelo com uma especialização? Nem mesmo para especializações explícitas?
dyp
2
Argumentos de modelo padrão w / r / t, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC
1
@dyp Algumas semanas depois e lendo muito sobre isso e com uma dica deste trecho, acho que começo a entender como isso funciona. Sua explicação faz da leitura para ler mais sentido para mim, obrigado!
Nonsensation
1
Eu queria acrescentar, que o termo principal modelo foi a chave (os modelos primeiro encontro no código)
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
{ };

A especialização acima existe apenas quando é bem formada, portanto, quando decltype( T::member )é válida e não ambígua. a especialização é assim has_member<T , void>como estado no comentário.

Quando você escreve has_member<A>, é has_member<A, void>por causa do argumento do modelo padrão.

E nós temos especialização para has_member<A, void>(então herdar de true_type), mas não temos especialização para has_member<B, void>(então usamos a definição padrão: herdar de false_type)

Jarod42
fonte