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?
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?
Respuestas:
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>::Yes
se 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.
...
, 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/…
Me gusta usar SFINAE
para 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 C
es 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.
M <= N ? 1 : -1
podría funcionar.
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 ).
error C2466: cannot allocate an array of constant size 0
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í.
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&>
?
TypeSink
, C ++ 17 tiene std::void_t
:)
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.
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_t
y 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.
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 value
valor de para ver si T
es 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;
}
int C::*
en su respuesta? ¿Cómo puede C::*
ser un nombre de parámetro?
int C::*
es el tipo de puntero a una int
variable miembro de C
.
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);
}
declval
es una utilidad que le da una "referencia falsa" a un objeto de un tipo que no se puede construir fácilmente. declval
es 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';
}
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? )
#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 constexpr
truco de la variable:
constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;
Según el estándar C ++, constexpr
se 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? ).
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