Eu assisti a palestra de Walter Brown no Cppcon14 sobre programação de modelos modernos ( Parte I , Parte II ), onde ele apresentou sua void_t
técnica SFINAE.
Exemplo:
Dado um modelo de variável simples que avalia void
se 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
existedecltype( A::member )
é bem formadovoid_t<>
é válido e avalia comovoid
has_member< A , void >
e, portanto, escolhe o modelo especializadohas_member< T , void >
e avalia paratrue_type
2) has_member< B >
has_member< B , void_t< decltype( B::member ) > >
B::member
não existedecltype( 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 comofalse_type
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_t
para que ele funcione. Por que é que? (Não vejo por que esses tipos precisam corresponder, apenas um tipo padrão não funciona?)
has_member<A,int>::value
. Em seguida, a especialização parcial avaliadahas_member<A,void>
não pode corresponder. Portanto, ele precisa serhas_member<A,void>::value
, ou, com açúcar sintático, um argumento padrão do tipovoid
.has_member< T , class = void >
padrãovoid
ainda. 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?template <class, class = void>
paratemplate <class, class = void_t<>>
. Então agora somos livres para fazer o que quisermos comvoid_t
a implementação do modelo pseudônimo :)Respostas:
1. Modelo de Classe Primária
Quando você escreve
has_member<A>::value
, o compilador consulta o nomehas_member
e encontra o modelo de classe principal , ou seja, esta declaração:(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 escritohas_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:O compilador tenta combinar os argumentos do modelo
A, void
com os padrões definidos na especialização parcial:T
evoid_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-modeloT
. Esta é uma dedução trivial, mas considere um padrão comoT const&
, onde ainda podemos deduzirT
. Para o padrãoT
e o argumento do modeloA
, deduzimosT
serA
.No segundo padrão
void_t< decltype( T::member ) >
, o parâmetro do modeloT
aparece em um contexto em que não pode ser deduzido de nenhum argumento do modelo.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:
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: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:
Terminamos com a mesma especialização:
mas a nossa lista de argumentos de modelos por
has_member<A>::value
enquanto é<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:
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.
fonte
A especialização acima existe apenas quando é bem formada, portanto, quando
decltype( T::member )
é válida e não ambígua. a especialização é assimhas_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 detrue_type
), mas não temos especialização parahas_member<B, void>
(então usamos a definição padrão: herdar defalse_type
)fonte