¿Cómo usamos void_t para SFINAE?

13 minutos de lectura

avatar de usuario de nonsensation
tontería

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

Ejemplo:
Dada una plantilla de 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 un miembro de datos llamado 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 lo tanto 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 > 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 a false_type

http://ideone.com/HCTlBb

Preguntas:

  1. ¿Es correcto mi entendimiento de esto?
  2. Walter Brown afirma que el argumento predeterminado tiene que ser exactamente del mismo tipo que el que se usa en void_t para que funcione. ¿Porqué es eso? (No veo por qué estos tipos deben coincidir, ¿cualquier tipo predeterminado no hace el trabajo?)

  • Anuncio 2) Imagina que la afirmación estática se escribió como: has_member<A,int>::value. Entonces, la especialización parcial que evalúa a has_member<A,void> no puede coincidir. Por lo tanto, necesita ser has_member<A,void>::valueo, con azúcar sintáctico, un argumento predeterminado de tipo void.

    – mojar

    29 de diciembre de 2014 a las 10:57

  • @dyp Gracias, lo editaré. Mh, no veo la necesidad de tener has_member< T , class = void > incumplir en void todavía. 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?

    – tonterías

    29 de diciembre de 2014 a las 11:29

  • Tenga en cuenta que en esta propuesta, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdfWalter cambió template <class, class = void> a template <class, class = void_t<>>. Así que ahora somos libres de hacer lo que queramos con void_t implementación de plantilla de alias 🙂

    – MaxPlancton

    8 de marzo de 2018 a las 17:33

avatar de usuario de dyp
mojar

1. Plantilla de clase primaria

Cuando escribes has_member<A>::valueel compilador busca el nombre has_member y encuentra el primario plantilla de clase, 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 la plantilla <A> se compara con la lista de parámetros de plantilla de esta plantilla principal. Dado que la plantilla principal tiene dos parámetros, pero usted solo proporcionó uno, el parámetro restante tiene como valor predeterminado el argumento de la plantilla predeterminada: void. Es como si hubieras escrito has_member<A, void>::value.

2. Plantilla de clase especializada

Ahorala lista de parámetros de la plantilla se compara con cualquier especialización de la plantilla has_member. Solo si ninguna especialización coincide, la definición de la plantilla principal se usa como alternativa. Por lo que se tiene en cuenta la especialización parcial:

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 a uno. Primero, se realiza la deducción del argumento de la plantilla. La especialización parcial anterior sigue siendo una plantilla con parámetros de plantilla que deben “llenarse” con argumentos.

el primer patrón Tpermite 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 la plantilla Adeducimos T ser A.

En el segundo patrón void_t< decltype( T::member ) >el parámetro de plantilla T aparece en un contexto en el que no se puede deducir de ningún argumento de plantilla.

Hay dos razones para esto:

  • La expresión interior 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 decltype como void_t< T >entonces la deducción de T ocurre en la plantilla de alias resuelta. Es decir, resolvemos la plantilla de alias y luego tratamos de deducir el tipo T del patrón resultante. El patrón resultante, sin embargo, es voidque no depende de T y por lo tanto no nos permite encontrar un tipo específico para T. Esto es similar al problema matemático de intentar invertir una función constante (en el sentido matemático de esos términos).

La deducción del argumento de la plantilla ha terminado. , ahora el deducido

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

se sustituyen los argumentos de la plantilla. Esto crea una especialización que se ve así: void_t< decltype( A::member ) > El tipo ya se puede evaluar. Está bien formado después de la sustitución, por lo tanto, no Fallo de sustitución

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

ocurre. Obtenemos:

3. ElecciónAhora has_member<A>::valuepodemos comparar la lista de parámetros de plantilla de esta especialización con los argumentos de plantilla suministrados al original


. Ambos tipos coinciden exactamente, por lo que se elige esta especialización parcial.

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

Por otro lado, cuando definimos la plantilla como:

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

Terminamos con la misma especialización: has_member<A>::value pero nuestra lista de argumentos de plantilla para <A, int>ahora es


. Los argumentos no coinciden con los parámetros de la especialización y la plantilla principal se elige como alternativa. El Estándar, en mi humilde opinión de manera confusa, incluye el proceso de sustitución y la coincidencia de argumentos de plantilla explícitamente especificados en el [temp.class.spec.match]deducción de argumento de plantilla

proceso. Por ejemplo (posterior a N4296)

/2: Una especialización parcial coincide con una lista de argumentos de plantilla real determinada si los argumentos de plantilla de la especialización parcial se pueden deducir de la lista de argumentos de plantilla real. Pero esto no justo significa que todos los parámetros de plantilla de la especialización parcial tienen que ser deducidos; 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 (sustituida) 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 proporcionada.

    ¡Gracias! Lo he leído una y otra vez, y supongo que mi pensamiento sobre 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.

  • – tonterías

    30 de diciembre de 2014 a las 12:12

    @JohannesSchaub-litb ¡Gracias! Aunque eso es un poco deprimente. ¿Realmente no hay reglas para hacer coincidir un argumento de plantilla con una especialización? ¿Ni siquiera para especializaciones explícitas?

  • – mojar 30 de diciembre de 2014 a las 16:27

    W/r/t argumentos de plantilla predeterminados,

    open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008

  • – CT 28 de enero de 2015 a las 10:41 @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 esto. Tu explicación hace que de lectura en lectura tenga más sentido para mí, ¡gracias!

  • – tonterías 25 de febrero de 2015 a las 23:05 Quería agregar que el término

    primario

    la plantilla fue la clave (las plantillas se encuentran por primera vez en el código)

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

– tontería decltype( T::member ) 27/09/2016 a las 12:51 has_member<T , void> Esa especialización anterior existe solo cuando está bien formada, por lo que cuando

es válido y no ambiguo. la especialización es así para has_member<A>como se indica en el comentario. has_member<A, void> Cuando escribes

es has_member<A, void> debido al argumento de la plantilla predeterminada. true_typeY tenemos especialización para has_member<B, void> (así que heredar de false_type) pero no tenemos especialización para

  • (así que usamos la definición predeterminada: heredar de void_t<decltype(T::member)>> ) decltype(T::member, void())Entonces

    no es más que una forma estandarizada/más clara de escribir

    ?

  • – 303 void_t 5 de diciembre de 2021 a las 21:26

    @ 303: alguna versión (antigua) del compilador tuvo dificultades para aplicar SFINAE con (algunas implementaciones de)

    aunque.

– Jarod42
6 de diciembre de 2021 a las 8:41

avatar de usuario de user3059627

usuario3059627 Este hilo y el hilo SFINAE: Entender void_t y detectar_si me salvaron. Quiero demostrar el comportamiento con algunos ejemplos:

La herramienta:

struct A {
    using type = int;
};

struct B{
    using type = void;
}

cppinsights

auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;

Para probar la implementación por tipos de flotador y los siguientes tipos:

Probado por implementación estándar

#include <type_traits>

// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };

// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { }; 

De

bool f = false;
bool a = true;
bool b = true;

bool x = has_type_member<A, int>::value; //x = false;

esta referencia std::void_t

#include <type_traits>

// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };

template< class T >
struct has_type_member<T, void> : std::true_type { };

// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { }; 

Producción

/home/insights/insights.cpp:14:8: error: redefinition of 'has_type_member<T, std::void_t<typename T::type>>'
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };
       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/insights/insights.cpp:8:8: note: previous definition is here
struct has_type_member<T, void>: std::true_type {};
       ^
1 error generated.
Error while processing /home/insights/insights.cpp.

caso 1 has_type_member<T, std::void_t<typename T::type>> Producción has_type_member Entonces, has_type_member<T, void>definió una especialización de

y la firma es exactamente

#include <type_traits>

template< class, class = void >
struct has_type_member : std::false_type { };

// specialize 2nd type as void
template< class T>
struct has_type_member<T, void> : std::true_type { };

.

bool f = true;
bool a = true;
bool b = true;

caso 2

  1. Producción: has_type_member<float>
  2. Este caso muestra que el compilador: has_type_member<float, void>
  3. Quería encontrar una pareja para std::true_type

Descubrí que la plantilla requiere 2 argumentos, luego llené el segundo argumento con los argumentos predeterminados. La estructura era como

#include <type_traits>

template< class, class = void >
struct has_type_member : std::false_type { };

template<class T>
struct has_type_member<T, typename T::type>: std::true_type {}; 

Encontré una especialización de esta firma y obtuve el valor de

bool f = false;
bool a = false;
bool b = true;

caso 3

  1. has_type_member<float> Producción: has_type_member<float, void>caso f
  2. se completó en typename float::type .
  3. Entonces el compilador intentó

y falló

  1. has_type_member<A> La plantilla principal elegida. has_type_member<A, void>
  2. caso un has_type_member<A, typename A::type> se completó en has_type_member<A, int>
  3. Entonces el compilador intentó has_type_member<A, void>
  4. y descubrí que era

El compilador decidió que no era una especialización de

  1. has_type_member<B> Luego se eligió la plantilla principal. has_type_member<B, void>caso b
  2. se completó en has_type_member<B, typename B::type> . has_type_member<B, void>Entonces el compilador intentó
  3. y descubrí que era has_type_member<B, void>
  4. true_type .

El compilador decidió que era una especialización de

#include <type_traits>

//int as default 2nd argument
template< class, class = int >
struct has_type_member : std::false_type { };

template<class T>
struct has_type_member<T, std::void<typename T::type>>: std::true_type {}; 

escogido.

bool f = false;
bool a = false;
bool b = false;

caso 4 has_type_member<T> Producción: has_type_member<T, int> El true_type es de tipo has_type_member<T, void> para las 3 variables, mientras que el

tiene firma como

si es valido std::void_tConclusión

  1. Entonces el T::type :
  2. Comprobar si

  • válido. std::voidProporciona una especialización de la plantilla principal si solo se proporciona un argumento de plantilla.

    qué es

    ?


¿Ha sido útil esta solución?