¿Cómo funciona `void_t`

149

Vi la charla de Walter Brown en Cppcon14 sobre la programación de plantillas modernas ( Parte I , Parte II ) donde presentó su void_ttécnica SFINAE.

Ejemplo:
Dada una plantilla variable simple que evalúa voidsi todos los argumentos de la plantilla están bien formados:

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

y el siguiente rasgo que verifica la existencia de una variable miembro llamada miembro :

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

Traté de entender por qué y cómo funciona esto. Por lo tanto, un pequeño ejemplo:

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 ) está bien formado
    • void_t<> es válido y se evalúa como void
  • has_member< A , void > y por eso elige la plantilla especializada
  • has_member< T , void > y evalúa a true_type

2) has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member no existe
    • decltype( B::member ) está mal formado y falla en silencio (sfinae)
    • has_member< B , expression-sfinae > entonces esta plantilla se descarta
  • el compilador encuentra has_member< B , class = void >con vacío como argumento predeterminado
  • has_member< B > evalúa a false_type

http://ideone.com/HCTlBb

Preguntas:
1. ¿Es correcto entender esto?
2. Walter Brown afirma que el argumento predeterminado tiene que ser exactamente del mismo tipo que el utilizado void_tpara que funcione. ¿Porqué es eso? (No veo por qué estos tipos deben coincidir, ¿no funciona cualquier tipo predeterminado?)

sin sentido
fuente
66
Ad 2) Imagine que la aserción estática fue escrito como: has_member<A,int>::value. Entonces, la especialización parcial que evalúa has_member<A,void>no puede coincidir. Por lo tanto, debe ser has_member<A,void>::value, o, con azúcar sintáctico, un argumento de tipo predeterminado void.
dyp
1
@dyp Gracias, lo editaré. Mh, todavía no veo la necesidad de tener un has_member< T , class = void >incumplimiento void. Suponiendo que este rasgo se usará solo con 1 argumento de plantilla en cualquier momento, entonces el argumento predeterminado podría ser de cualquier tipo.
sentido
Interesante pregunta.
AStopher
2
Tenga en cuenta que en esta propuesta, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Walter cambió template <class, class = void>a template <class, class = void_t<>>. Así que ahora somos libres de hacer lo que queramos con void_tla implementación de plantillas de alias :)
JohnKoch

Respuestas:

133

1. Plantilla de clase primaria

Cuando escribe has_member<A>::value, el compilador busca el nombre has_membery encuentra la plantilla de clase primaria , es decir, esta declaración:

template< class , class = void >
struct has_member;

(En el OP, eso está escrito como una definición).

La lista de argumentos de plantilla <A>se compara con la lista de parámetros de plantilla de esta plantilla primaria. Dado que la plantilla principal tiene dos parámetros, pero sólo se suministra uno, el parámetro restante está en mora al argumento de plantilla por defecto: void. Es como si hubieras escrito has_member<A, void>::value.

2. Plantilla de clase especializada

Ahora , la lista de parámetros de la plantilla se compara con cualquier especialización de la plantilla has_member. Solo si no coincide la especialización, la definición de la plantilla primaria se usa como una alternativa. Entonces, la especialización parcial se tiene en cuenta:

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

El compilador intenta hacer coincidir los argumentos de la plantilla A, voidcon los patrones definidos en la especialización parcial: Ty void_t<..>uno por uno. Primero , se realiza la deducción de argumento de plantilla. La especialización parcial anterior sigue siendo una plantilla con parámetros de plantilla que deben "rellenarse" con argumentos.

El primer patrón T , permite al compilador deducir el parámetro de plantilla T. Esta es una deducción trivial, pero considere un patrón como T const&, donde aún podríamos deducir T. Para el patrón Ty el argumento de plantilla A, deducimos Tser A.

En el segundo patrón void_t< decltype( T::member ) > , el parámetro de plantilla Taparece en un contexto donde no puede deducirse de ningún argumento de plantilla.

Hay dos razones para esto:

  • La expresión dentro decltypeestá explícitamente excluida de la deducción de argumentos de plantilla. Supongo que esto se debe a que puede ser arbitrariamente complejo.

  • Incluso si usamos un patrón sin decltypelike void_t< T >, la deducción Tocurre en la plantilla de alias resuelto. Es decir, resolvemos la plantilla de alias y luego intentamos deducir el tipo Tdel patrón resultante. Sin embargo, el patrón resultante es el voidque no depende Ty, por lo tanto, no nos permite encontrar un tipo específico para T. Esto es similar al problema matemático de tratar de invertir una función constante (en el sentido matemático de esos términos).

Deducción argumento de plantilla está terminado (*) , ahora las deducidas argumentos de plantilla son sustituidos. Esto crea una especialización que se ve así:

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

El tipo void_t< decltype( A::member ) >ahora se puede evaluar. Está bien formado después de la sustitución, por lo tanto, no ocurre una falla de sustitución . Obtenemos:

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

3. Elección

Ahora , podemos comparar la lista de parámetros de plantilla de esta especialización con los argumentos de plantilla suministrados al original has_member<A>::value. Ambos tipos coinciden exactamente, por lo que se elige esta especialización parcial.


Por otro lado, cuando definimos la plantilla 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 con la misma especialización:

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

pero nuestra lista de argumentos de plantilla por has_member<A>::valueahora es <A, int>. Los argumentos no coinciden con los parámetros de la especialización, y la plantilla primaria se elige como una alternativa.


(*) La Norma, en mi humilde opinión, incluye el proceso de sustitución y la coincidencia de argumentos de plantilla especificados explícitamente en el proceso de deducción de argumentos de plantilla . Por ejemplo (post-N4296) [temp.class.spec.match] / 2:

Una especialización parcial coincide con una lista de argumentos de plantilla real dada si los argumentos de plantilla de la especialización parcial pueden deducirse de la lista de argumentos de plantilla real.

Pero esto no solo significa que todos los parámetros de plantilla de la especialización parcial deben deducirse; también significa que la sustitución debe tener éxito y (como parece) los argumentos de la plantilla tienen que coincidir con los parámetros de la plantilla (sustituidos) de la especialización parcial. Tenga en cuenta que no estoy completamente al tanto de dónde el Estándar especifica la comparación entre la lista de argumentos sustituidos y la lista de argumentos suministrada.

dyp
fuente
3
¡Gracias! Lo he leído una y otra vez, y creo que mi pensamiento de cómo funciona exactamente la deducción de argumentos de plantilla y qué elige el compilador para la plantilla final no es correcto en este momento.
sentido
1
@ JohannesSchaub-litb ¡Gracias! Sin embargo, eso es un poco deprimente. ¿Realmente no hay reglas para unir un argumento de plantilla con una especialización? ¿Ni siquiera para especializaciones explícitas?
dyp
2
W / r / t argumentos de plantilla predeterminados, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC
1
@dyp Unas semanas más tarde y leyendo mucho sobre esto y con una pista de este fragmento, creo que empiezo a entender cómo funciona. Su explicación tiene sentido para leer y leer, ¡gracias!
sentido
1
Quería agregar que el término plantilla principal era la clave (las plantillas se encuentran por primera vez en el código)
sentido
18
// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };

Esa especialización anterior solo existe cuando está bien formada, por lo que decltype( T::member )es válida y no ambigua. La especialización es así has_member<T , void>como el estado en el comentario.

Cuando escribe has_member<A>, se has_member<A, void>debe al argumento de plantilla predeterminado.

Y tenemos especialización para has_member<A, void>(heredar de true_type) pero no tenemos especialización para has_member<B, void>(así que usamos la definición predeterminada: heredar de false_type)

Jarod42
fuente