Namespaces
Variants

SFINAE

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

"La Sustitución de Falla No Es un Error"


Esta regla se aplica durante la resolución de sobrecarga de plantillas de función: Cuando la sustitución del tipo especificado explícitamente o deducido para el parámetro de plantilla falla, la especialización se descarta del conjunto de sobrecarga en lugar de causar un error de compilación.

Esta característica se utiliza en metaprogramación de plantillas.

Contenidos

Explicación

Los parámetros de plantilla de función se sustituyen (reemplazados por argumentos de plantilla) dos veces:

  • los argumentos de plantilla especificados explícitamente se sustituyen antes de la deducción de argumentos de plantilla
  • los argumentos deducidos y los argumentos obtenidos de los valores predeterminados se sustituyen después de la deducción de argumentos de plantilla

La sustitución ocurre en

  • todos los tipos utilizados en el tipo de función (que incluye el tipo de retorno y los tipos de todos los parámetros)
  • todos los tipos utilizados en las declaraciones de parámetros de plantilla
  • todos los tipos utilizados en la lista de argumentos de plantilla de una especialización parcial
  • todas las expresiones utilizadas en el tipo de función
  • todas las expresiones utilizadas en una declaración de parámetro de plantilla
  • todas las expresiones utilizadas en la lista de argumentos de plantilla de una especialización parcial
(desde C++11)
(desde C++20)

Un fallo de sustitución es cualquier situación en la que el tipo o expresión anterior estaría mal formado (con un diagnóstico requerido), si se escribiera utilizando los argumentos sustituidos.

Solo los fallos en los tipos y expresiones en el contexto inmediato del tipo de función o sus tipos de parámetros de plantilla o su especificador explícito (desde C++20) son errores SFINAE. Si la evaluación de un tipo/expresión sustituido causa un efecto secundario como la instanciación de alguna especialización de plantilla, generación de una función miembro definida implícitamente, etc., los errores en esos efectos secundarios se tratan como errores graves. Una expresión lambda no se considera parte del contexto inmediato. (desde C++20)

La sustitución procede en orden léxico y se detiene cuando se encuentra un fallo.

Si existen múltiples declaraciones con diferentes órdenes léxicos (por ejemplo, una plantilla de función declarada con tipo de retorno final, que se sustituirá después de un parámetro, y redeclarada con tipo de retorno ordinario que se sustituiría antes del parámetro), y eso causaría que las instanciaciones de plantillas ocurrieran en un orden diferente o no ocurrieran en absoluto, entonces el programa está mal formado; no se requiere diagnóstico.

(since C++11)
template<typename A>
struct B { using type = typename A::type; };
template<
    class T,
    class U = typename T::type,    // Fallo de SFINAE si T no tiene miembro type
    class V = typename B<T>::type> // Error grave si B no tiene miembro type
                                   // (garantizado que no ocurrirá vía CWG 1227 porque
                                   // la sustitución en el argumento de plantilla por defecto
                                   // de U fallaría primero)
void foo (int);
template<class T>
typename T::type h(typename B<T>::type);
template<class T>
auto h(typename B<T>::type) -> typename T::type; // redeclaración
template<class T>
void h(...) {}
using R = decltype(h<int>(0));     // incorrecto, no se requiere diagnóstico

SFINAE de Tipo

Los siguientes errores de tipo son errores SFINAE:

  • intentar instanciar una expansión de paquete que contiene múltiples paquetes de longitudes diferentes
(since C++11)
  • intentando crear un array de void, array de referencia, array de función, array de tamaño negativo, array de tamaño no integral, o array de tamaño cero:
template<int I>
void div(char(*)[I % 2 == 0] = nullptr)
{
    // esta sobrecarga se selecciona cuando I es par
}
template<int I>
void div(char(*)[I % 2 == 1] = nullptr)
{
    // esta sobrecarga se selecciona cuando I es impar
}
  • intentando usar un tipo a la izquierda de un operador de resolución de ámbito :: y no es una clase o enumeración:
template<class T>
int f(typename T::B*);
template<class T>
int f(T);
int i = f<int>(0); // utiliza la segunda sobrecarga
  • intentando usar un miembro de un tipo, donde
  • el tipo no contiene el miembro especificado
  • el miembro especificado no es un tipo donde se requiere un tipo
  • el miembro especificado no es una plantilla donde se requiere una plantilla
  • el miembro especificado no es un no-tipo donde se requiere un no-tipo
template<int I>
struct X {};
template<template<class T> class>
struct Z {};
template<class T>
void f(typename T::Y*) {}
template<class T>
void g(X<T::N>*) {}
template<class T>
void h(Z<T::template TT>*) {}
struct A {};
struct B { int Y; };
struct C { typedef int N; };
struct D { typedef int TT; };
struct B1 { typedef int Y; };
struct C1 { static const int N = 0; };
struct D1
{ 
    template<typename T>
    struct TT {}; 
};
int main()
{
    // La deducción falla en cada uno de estos casos:
    f<A>(0); // A no contiene un miembro Y
    f<B>(0); // El miembro Y de B no es un tipo
    g<C>(0); // El miembro N de C no es un no-tipo
    h<D>(0); // El miembro TT de D no es una plantilla
    // La deducción tiene éxito en cada uno de estos casos:
    f<B1>(0); 
    g<C1>(0); 
    h<D1>(0);
}
// por hacer: necesita demostrar resolución de sobrecarga, no solo fallo
  • intentando crear un puntero a referencia
  • intentando crear una referencia a void
  • intentando crear puntero a miembro de T, donde T no es un tipo clase:
template<typename T>
class is_class
{
    typedef char yes[1];
    typedef char no[2];
    template<typename C>
    static yes& test(int C::*); // seleccionado si C es un tipo clase
    template<typename C>
    static no& test(...);       // seleccionado en caso contrario
public:
    static bool const value = sizeof(test<T>(nullptr)) == sizeof(yes);
};
  • intentar asignar un tipo inválido a un parámetro de plantilla constante:
template<class T, T>
struct S {};
template<class T>
int f(S<T, T()>*);
struct X {};
int i0 = f<X>(0);
// por hacer: necesita demostrar resolución de sobrecarga, no solo fallo
  • intentando realizar una conversión inválida en
  • en una expresión de argumento de plantilla
  • en una expresión utilizada en declaración de función:
template<class T, T*> int f(int);
int i2 = f<int, 1>(0); // no se puede convertir 1 a int*
// por hacer: necesita demostrar resolución de sobrecarga, no solo fallo
  • intentando crear un tipo de función con un parámetro de tipo void
  • intentando crear un tipo de función que retorna un tipo array o un tipo función

SFINAE de Expresión

Solo las expresiones constantes utilizadas en tipos (como límites de arreglos) debían tratarse como SFINAE (y no como errores graves) antes de C++11.

(until C++11)

Los siguientes errores de expresión son errores SFINAE

  • Expresión mal formada utilizada en un tipo de parámetro de plantilla
  • Expresión mal formada utilizada en el tipo de función:
struct X {};
struct Y { Y(X){} }; // X is convertible to Y
template<class T>
auto f(T t1, T t2) -> decltype(t1 + t2); // overload #1
X f(Y, Y);                               // overload #2
X x1, x2;
X x3 = f(x1, x2); // deduction fails on #1 (expression x1 + x2 is ill-formed)
                  // only #2 is in the overload set, and is called
(since C++11)

SFINAE en especializaciones parciales

La deducción y sustitución también ocurren al determinar si una especialización de una clase o variable (since C++14) template es generada por alguna partial specialization o la plantilla principal. Un fallo de sustitución no se trata como un error grave durante dicha determinación, sino que hace que la declaración de especialización parcial correspondiente se ignore, como en la resolución de sobrecarga que involucra plantillas de función.

// la plantilla principal maneja tipos no referenciables:
template<class T, class = void>
struct reference_traits
{
    using add_lref = T;
    using add_rref = T;
};
// la especialización reconoce tipos referenciables:
template<class T>
struct reference_traits<T, std::void_t<T&>>
{
    using add_lref = T&;
    using add_rref = T&&;
};
template<class T>
using add_lvalue_reference_t = typename reference_traits<T>::add_lref;
template<class T>
using add_rvalue_reference_t = typename reference_traits<T>::add_rref;

Soporte de biblioteca

El componente de la biblioteca estándar std::enable_if permite crear un fallo de sustitución para habilitar o deshabilitar sobrecargas específicas basándose en una condición evaluada en tiempo de compilación.

Además, muchos type traits deben implementarse con SFINAE si las extensiones de compilador apropiadas no están disponibles.

(since C++11)

El componente de la biblioteca estándar std::void_t es otra metafunción de utilidad que simplifica las aplicaciones de especialización parcial SFINAE.

(desde C++17)

Alternativas

Donde sea aplicable, tag dispatch , if constexpr (desde C++17) , y concepts (desde C++20) generalmente se prefieren sobre el uso de SFINAE.

static_assert generalmente se prefiere sobre SFINAE si solo se desea un error condicional en tiempo de compilación.

(desde C++11)

Ejemplos

Un idiom común es usar SFINAE de expresión en el tipo de retorno, donde la expresión utiliza el operador coma, cuyo subexpresión izquierda es la que se está examinando (convertida a void para asegurar que no se seleccione el operador coma definido por el usuario en el tipo de retorno), y el subexpresión derecha tiene el tipo que la función debe retornar.

#include <iostream>
// This overload is added to the set of overloads if C is
// a class or reference-to-class type and F is a pointer to member function of C
template<class C, class F>
auto test(C c, F f) -> decltype((void)(c.*f)(), void())
{
    std::cout << "(1) Class/class reference overload called\n";
}
// This overload is added to the set of overloads if C is a
// pointer-to-class type and F is a pointer to member function of C
template<class C, class F>
auto test(C c, F f) -> decltype((void)((c->*f)()), void())
{
    std::cout << "(2) Pointer overload called\n";
}
// This overload is always in the set of overloads: ellipsis
// parameter has the lowest ranking for overload resolution
void test(...)
{
    std::cout << "(3) Catch-all overload called\n";
}
int main()
{
    struct X { void f() {} };
    X x;
    X& rx = x;
    test(x, &X::f);  // (1)
    test(rx, &X::f); // (1), creates a copy of x
    test(&x, &X::f); // (2)
    test(42, 1337);  // (3)
}

Salida:

(1) Class/class reference overload called
(1) Class/class reference overload called
(2) Pointer overload called
(3) Catch-all overload called

Informes de defectos

Los siguientes informes de defectos que modifican el comportamiento se aplicaron retroactivamente a los estándares de C++ publicados anteriormente.

DR Aplicado a Comportamiento publicado Comportamiento correcto
CWG 295 C++98 la creación de tipos de función calificados cv
podía resultar en fallo de sustitución
se convierte en no fallo,
descartando la calificación cv
CWG 1227 C++98 el orden de sustitución no estaba especificado igual al orden léxico
CWG 2054 C++98 la sustitución en especializaciones parciales no estaba correctamente especificada especificada
CWG 2322 C++11 declaraciones en diferentes órdenes léxicos causarían que las instanciaciones
de plantilla ocurrieran en un orden diferente o no ocurrieran
tal caso está mal formado,
no se requiere diagnóstico