c++ - studio - ¿Cómo funciona `void_t`
wp_nav_menu example (2)
Cuando escribe
has_member<A>::value
, el compilador busca el nombre
has_member
y 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 solo proporcionó uno, el parámetro restante está predeterminado en el argumento de plantilla predeterminado:
void
.
Es como si hubiera escrito
has_member<A, void>::value
.
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 utiliza 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, void
con los patrones definidos en la especialización parcial:
T
y
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
T
y el argumento de plantilla
A
, deducimos que
T
es
A
En el segundo patrón
void_t< decltype( T::member ) >
, el parámetro de plantilla
T
aparece en un contexto donde no puede deducirse de ningún argumento de plantilla.
Hay dos razones para esto:
-
La expresión dentro de
decltype
se excluye explícitamente 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 tipo de
decltype
comovoid_t< T >
, la deducción deT
ocurre en la plantilla de alias resuelto. Es decir, resolvemos la plantilla de alias y luego intentamos deducir el tipoT
del patrón resultante. Sin embargo, el patrón resultante esvoid
, que no depende deT
y, por lo tanto, no nos permite encontrar un tipo específico paraT
Esto es similar al problema matemático de tratar de invertir una función constante (en el sentido matemático de esos términos).
La deducción de argumentos de plantilla ha finalizado (*) , ahora se sustituyen los argumentos de plantilla deducidos . 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
{ };
Ahora, podemos comparar la lista de parámetros de plantilla de esta especialización con los argumentos de plantilla suministrados al valor 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 para
has_member<A>::value
ahora 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 se deben deducir todos los parámetros de plantilla de la especialización parcial; también significa que la sustitución debe tener éxito y (como parece) los argumentos de la plantilla deben coincidir con los parámetros de la plantilla (sustituidos) de la especialización parcial. Tenga en cuenta que no soy completamente consciente de dónde el Estándar especifica la comparación entre la lista de argumentos sustituidos y la lista de argumentos suministrada.
Vi la charla de Walter Brown en Cppcon14 sobre la programación moderna de plantillas (
Parte I
,
Parte II
) donde presentó su técnica
void_t
SFINAE.
Ejemplo:
Dada una plantilla variable simple que se evalúa como
void
si 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 comovoid
-
-
has_member< A , void >
y, por lo tanto, elige la plantilla especializada -
has_member< T , void >
y se evalúa comotrue_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 >
por lo que esta plantilla se descarta
-
-
el compilador encuentra
has_member< B , class = void >
con void como argumento predeterminado -
has_member< B >
evalúa comofalse_type
Preguntas:
1. ¿Es correcto entender esto?
2. Walter Brown afirma que el argumento predeterminado debe ser exactamente del mismo tipo que el utilizado en
void_t
para que funcione.
¿Porqué es eso?
(No veo por qué estos tipos deben coincidir, ¿no funciona cualquier tipo predeterminado?)
// 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 cuando
decltype( T::member )
es válido y no ambiguo.
la especialización es así para
has_member<T , void>
como estado en el comentario.
Cuando escribe
has_member<A>
, es
has_member<A, void>
debido al argumento de plantilla predeterminado.
Y tenemos especialización para
has_member<A, void>
(así que hereda de
true_type
) pero no tenemos especialización para
has_member<B, void>
(así que usamos la definición predeterminada: heredar de
false_type
)