Ejemplos de C ++ SFINAE?

122

Quiero entrar en más metaprogramación de plantillas. Sé que SFINAE significa "la falla de sustitución no es un error". Pero, ¿alguien puede mostrarme un buen uso de SFINAE?

rlbond
fuente
2
Esta es una buena pregunta. Entiendo SFINAE bastante bien, pero no creo que haya tenido que usarlo nunca (a menos que las bibliotecas lo estén haciendo sin que yo lo sepa).
Zifre
5
STL lo expresó de manera ligeramente diferente en las preguntas frecuentes aquí , "La falla de sustitución no es un elefante"
vulcan raven

Respuestas:

72

Aquí hay un ejemplo ( de aquí ):

template<typename T>
class IsClassT {
  private:
    typedef char One;
    typedef struct { char a[2]; } Two;
    template<typename C> static One test(int C::*);
    // Will be chosen if T is anything except a class.
    template<typename C> static Two test(...);
  public:
    enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 };
    enum { No = !Yes };
};

Cuando IsClassT<int>::Yesse evalúa, 0 no se puede convertir a int int::*porque int no es una clase, por lo que no puede tener un puntero de miembro. Si SFINAE no existiera, obtendría un error del compilador, algo como '0 no se puede convertir en puntero de miembro para el tipo int que no es de clase'. En cambio, solo usa el ...formulario que devuelve Two y, por lo tanto, se evalúa como falso, int no es un tipo de clase.

Greg Rogers
fuente
8
@rlbond, respondí su pregunta en los comentarios a esta pregunta aquí: stackoverflow.com/questions/822059/… . En resumen: si ambas funciones de prueba son candidatas y viables, entonces "..." tiene el peor costo de conversión y, por lo tanto, nunca se tomará a favor de la otra función. "..." es la elipsis, var-arg: int printf (char const *, ...);
Johannes Schaub - litb
20
Lo más extraño aquí, en mi opinión, no es el ..., sino el int C::*, que nunca había visto y tuve que ir a buscar. Encontré la respuesta para lo que es y para qué podría usarse aquí: stackoverflow.com/questions/670734/…
HostileFork dice que no confíes en SE
1
¿Alguien puede explicar qué es C :: *? Leí todos los comentarios y enlaces, pero todavía me pregunto, int C :: * significa que es un puntero de miembro de tipo int. ¿Qué pasa si una clase no tiene ningún miembro de tipo int? ¿Qué me estoy perdiendo? y ¿cómo juega la prueba <T> (0) en esto? Debo estar perdiendo algo
user2584960
92

Me gusta usar SFINAEpara verificar condiciones booleanas.

template<int I> void div(char(*)[I % 2 == 0] = 0) {
    /* this is taken when I is even */
}

template<int I> void div(char(*)[I % 2 == 1] = 0) {
    /* this is taken when I is odd */
}

Puede resultar muy útil. Por ejemplo, lo usé para verificar si una lista de inicializadores recopilada usando una coma de operador no es más larga que un tamaño fijo

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}

La lista solo se acepta cuando M es menor que N, lo que significa que la lista del inicializador no tiene demasiados elementos.

La sintaxis char(*)[C]significa: puntero a una matriz con el tipo de elemento char y tamaño C. Si Ces falso (0 aquí), entonces obtenemos el tipo no válido char(*)[0], puntero a una matriz de tamaño cero: SFINAE hace que la plantilla sea ignorada en ese momento.

Expresado con boost::enable_if, se ve así

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, 
           typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}

En la práctica, a menudo encuentro útil la capacidad de comprobar las condiciones.

Johannes Schaub - litb
fuente
1
@Johannes Curiosamente, GCC (4.8) y Clang (3.2) aceptan declarar matrices de tamaño 0 (por lo que el tipo no es realmente "inválido"), pero se comporta correctamente en su código. Probablemente exista un apoyo especial para este caso en el caso de SFINAE frente a usos "regulares" de tipos.
akim
@akim: si eso alguna vez es cierto (¡¿raro?! ¿Desde cuándo?) Entonces tal vez M <= N ? 1 : -1podría funcionar.
v.oddou
1
@ v.oddou Solo inténtalo int foo[0]. No me sorprende que sea compatible, ya que permite el truco muy útil de "estructura que termina con una matriz de longitud 0" ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
akim
@akim: sí, es lo que pensé -> C99. Esto no está permitido en C ++, esto es lo que obtiene con un compilador moderno:error C2466: cannot allocate an array of constant size 0
v.oddou
1
@ v.oddou No, realmente me refería a C ++, y en realidad a C ++ 11: tanto clang ++ como g ++ lo aceptan, y he señalado una página que explica por qué esto es útil.
Akim
16

En C ++ 11, las pruebas SFINAE se han vuelto mucho más bonitas. A continuación, se muestran algunos ejemplos de usos comunes:

Elija una sobrecarga de funciones en función de los rasgos

template<typename T>
std::enable_if_t<std::is_integral<T>::value> f(T t){
    //integral version
}
template<typename T>
std::enable_if_t<std::is_floating_point<T>::value> f(T t){
    //floating point version
}

Usando el llamado idioma de sumidero de tipos, puede hacer pruebas bastante arbitrarias en un tipo, como verificar si tiene un miembro y si ese miembro es de cierto tipo

//this goes in some header so you can use it everywhere
template<typename T>
struct TypeSink{
    using Type = void;
};
template<typename T>
using TypeSinkT = typename TypeSink<T>::Type;

//use case
template<typename T, typename=void>
struct HasBarOfTypeInt : std::false_type{};
template<typename T>
struct HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>> :
    std::is_same<typename std::decay<decltype(std::declval<T&>().*(&T::bar))>::type,int>{};


struct S{
   int bar;
};
struct K{

};

template<typename T, typename = TypeSinkT<decltype(&T::bar)>>
void print(T){
    std::cout << "has bar" << std::endl;
}
void print(...){
    std::cout << "no bar" << std::endl;
}

int main(){
    print(S{});
    print(K{});
    std::cout << "bar is int: " << HasBarOfTypeInt<S>::value << std::endl;
}

Aquí hay un ejemplo en vivo: http://ideone.com/dHhyHE También escribí recientemente una sección completa sobre SFINAE y envío de etiquetas en mi blog (enchufe descarado pero relevante) http://metaporky.blogspot.de/2014/08/ part-7-static-dispatch-function.html

Tenga en cuenta que a partir de C ++ 14 hay un std :: void_t que es esencialmente el mismo que mi TypeSink aquí.

odinthenerd
fuente
Su primer bloque de código redefine la misma plantilla.
TC
Dado que no hay ningún tipo para el que is_integral e is_floating_point sean ambos verdaderos, debería ser una opción o porque SFINAE eliminará al menos uno.
odinthenerd
Está redefiniendo la misma plantilla con diferentes argumentos de plantilla predeterminados. ¿Has intentado compilarlo?
TC
2
Soy nuevo en la metaprogramación de plantillas, así que quería entender este ejemplo. ¿Hay alguna razón por la que use TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>en un lugar y luego TypeSinkT<decltype(&T::bar)>en otro? ¿También es &necesario en std::declval<T&>?
Kevin Doyon
1
Acerca de su TypeSink, C ++ 17 tiene std::void_t:)
YSC
10

La biblioteca enable_if de Boost ofrece una interfaz limpia y agradable para usar SFINAE. Uno de mis ejemplos de uso favoritos está en la biblioteca Boost.Iterator . SFINAE se utiliza para habilitar conversiones de tipo de iterador.

David Joyner
fuente
4

C ++ 17 probablemente proporcionará un medio genérico para consultar características. Consulte N4502 para obtener más detalles, pero como ejemplo autónomo, considere lo siguiente.

Esta parte es la parte constante, ponla en un encabezado.

// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4502.pdf.
template <typename...>
using void_t = void;

// Primary template handles all types not supporting the operation.
template <typename, template <typename> class, typename = void_t<>>
struct detect : std::false_type {};

// Specialization recognizes/validates only types supporting the archetype.
template <typename T, template <typename> class Op>
struct detect<T, Op, void_t<Op<T>>> : std::true_type {};

El siguiente ejemplo, tomado de N4502 , muestra el uso:

// Archetypal expression for assignment operation.
template <typename T>
using assign_t = decltype(std::declval<T&>() = std::declval<T const &>())

// Trait corresponding to that archetype.
template <typename T>
using is_assignable = detect<T, assign_t>;

En comparación con las otras implementaciones, esta es bastante simple: un conjunto reducido de herramientas ( void_ty detect) es suficiente. Además, se informó (ver N4502 ) que es considerablemente más eficiente (tiempo de compilación y consumo de memoria del compilador) que los enfoques anteriores.

Aquí hay un ejemplo en vivo , que incluye ajustes de portabilidad para GCC pre 5.1.

akim
fuente
3

Aquí hay otro ejemplo de SFINAE (tardío) , basado en la respuesta de Greg Rogers :

template<typename T>
class IsClassT {
    template<typename C> static bool test(int C::*) {return true;}
    template<typename C> static bool test(...) {return false;}
public:
    static bool value;
};

template<typename T>
bool IsClassT<T>::value=IsClassT<T>::test<T>(0);

De esta manera, puede verificar el valuevalor de para ver si Tes una clase o no:

int main(void) {
    std::cout << IsClassT<std::string>::value << std::endl; // true
    std::cout << IsClassT<int>::value << std::endl;         // false
    return 0;
}
whoan
fuente
¿Qué significa esta sintaxis int C::*en su respuesta? ¿Cómo puede C::*ser un nombre de parámetro?
Kirill Kobelev
1
Es un puntero al miembro. Alguna referencia: isocpp.org/wiki/faq/pointers-to-members
whoan
@KirillKobelev int C::*es el tipo de puntero a una intvariable miembro de C.
YSC
3

Aquí hay un buen artículo de SFINAE: Una introducción al concepto SFINAE de C ++: introspección en tiempo de compilación de un miembro de la clase .

Resúmalo de la siguiente manera:

/*
 The compiler will try this overload since it's less generic than the variadic.
 T will be replace by int which gives us void f(const int& t, int::iterator* b = nullptr);
 int doesn't have an iterator sub-type, but the compiler doesn't throw a bunch of errors.
 It simply tries the next overload. 
*/
template <typename T> void f(const T& t, typename T::iterator* it = nullptr) { }

// The sink-hole.
void f(...) { }

f(1); // Calls void f(...) { }

template<bool B, class T = void> // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.

template<class T> // A specialisation used if the expression is true. 
struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.

template <class T> typename enable_if<hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
    return obj.serialize();
}

template <class T> typename enable_if<!hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
    return to_string(obj);
}

declvales una utilidad que le da una "referencia falsa" a un objeto de un tipo que no se puede construir fácilmente. declvales realmente útil para nuestras construcciones SFINAE.

struct Default {
    int foo() const {return 1;}
};

struct NonDefault {
    NonDefault(const NonDefault&) {}
    int foo() const {return 1;}
};

int main()
{
    decltype(Default().foo()) n1 = 1; // int n1
//  decltype(NonDefault().foo()) n2 = n1; // error: no default constructor
    decltype(std::declval<NonDefault>().foo()) n2 = n1; // int n2
    std::cout << "n2 = " << n2 << '\n';
}
zangw
fuente
0

Aquí, estoy usando la sobrecarga de funciones de plantilla (no directamente SFINAE) para determinar si un puntero es una función o un puntero de clase de miembro: ( ¿Es posible corregir los punteros de función de miembro de iostream cout / cerr que se imprimen como 1 o verdadero? )

https://godbolt.org/z/c2NmzR

#include<iostream>

template<typename Return, typename... Args>
constexpr bool is_function_pointer(Return(*pointer)(Args...)) {
    return true;
}

template<typename Return, typename ClassType, typename... Args>
constexpr bool is_function_pointer(Return(ClassType::*pointer)(Args...)) {
    return true;
}

template<typename... Args>
constexpr bool is_function_pointer(Args...) {
    return false;
}

struct test_debugger { void var() {} };
void fun_void_void(){};
void fun_void_double(double d){};
double fun_double_double(double d){return d;}

int main(void) {
    int* var;

    std::cout << std::boolalpha;
    std::cout << "0. " << is_function_pointer(var) << std::endl;
    std::cout << "1. " << is_function_pointer(fun_void_void) << std::endl;
    std::cout << "2. " << is_function_pointer(fun_void_double) << std::endl;
    std::cout << "3. " << is_function_pointer(fun_double_double) << std::endl;
    std::cout << "4. " << is_function_pointer(&test_debugger::var) << std::endl;
    return 0;
}

Huellas dactilares

0. false
1. true
2. true
3. true
4. true

Tal como está el código, podría (dependiendo de la "buena" voluntad del compilador) generar una llamada en tiempo de ejecución a una función que devolverá verdadero o falso. Si desea forzar la is_function_pointer(var)evaluación en el tipo de compilación (no se realizan llamadas a funciones en tiempo de ejecución), puede usar el constexprtruco de la variable:

constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;

Según el estándar C ++, constexprse garantiza que todas las variables se evaluarán en tiempo de compilación (Calculando la longitud de una cadena C en tiempo de compilación. ¿Es esto realmente un constexpr? ).

usuario
fuente
0

El siguiente código usa SFINAE para permitir que el compilador seleccione una sobrecarga en función de si un tipo tiene cierto método o no:

    #include <iostream>
    
    template<typename T>
    void do_something(const T& value, decltype(value.get_int()) = 0) {
        std::cout << "Int: " <<  value.get_int() << std::endl;
    }
    
    template<typename T>
    void do_something(const T& value, decltype(value.get_float()) = 0) {
        std::cout << "Float: " << value.get_float() << std::endl;
    }
    
    
    struct FloatItem {
        float get_float() const {
            return 1.0f;
        }
    };
    
    struct IntItem {
        int get_int() const {
            return -1;
        }
    };
    
    struct UniversalItem : public IntItem, public FloatItem {};
    
    int main() {
        do_something(FloatItem{});
        do_something(IntItem{});
        // the following fails because template substitution
        // leads to ambiguity 
        // do_something(UniversalItem{});
        return 0;
    }

Salida:

Flotador: 1
Int: -1
vaquero
fuente