Voglio entrare in più meta-programmazione dei modelli. So che SFINAE sta per "il fallimento della sostituzione non è un errore". Ma qualcuno può mostrarmi un buon uso di SFINAE?
Voglio entrare in più meta-programmazione dei modelli. So che SFINAE sta per "il fallimento della sostituzione non è un errore". Ma qualcuno può mostrarmi un buon uso di SFINAE?
Risposte:
Ecco un esempio ( da qui ):
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 };
};
Quando IsClassT<int>::Yes
viene valutato, 0 non può essere convertito in int int::*
perché int non è una classe, quindi non può avere un puntatore a membro. Se SFINAE non esistesse, si otterrebbe un errore del compilatore, qualcosa come "0 non può essere convertito in puntatore membro per tipo non di classe int". Invece, utilizza solo il ...
form che restituisce Two e quindi restituisce false, int non è un tipo di classe.
...
, ma piuttosto il int C::*
, che non avevo mai visto e ho dovuto cercare. Trovato la risposta per quello che è e quello che potrebbe essere utilizzato per qui: stackoverflow.com/questions/670734/...
Mi piace usare SFINAE
per controllare le condizioni booleane.
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 */
}
Può essere molto utile. Ad esempio, l'ho usato per verificare se un elenco di inizializzatori raccolti utilizzando la virgola dell'operatore non è più lungo di una dimensione fissa
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}
L'elenco viene accettato solo quando M è minore di N, il che significa che l'elenco degli inizializzatori non ha troppi elementi.
La sintassi char(*)[C]
significa: puntatore a un array con tipo di elemento char e size C
. Se C
è falso (0 qui), otteniamo il tipo non valido char(*)[0]
, puntatore a un array di dimensione zero: SFINAE fa in modo che il modello venga ignorato in quel momento.
Espresso con boost::enable_if
, assomiglia a questo
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i,
typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}
In pratica, trovo spesso la capacità di controllare le condizioni un'abilità utile.
M <= N ? 1 : -1
potrebbe funzionare invece.
int foo[0]
. Non sono sorpreso che sia supportato, in quanto consente l'utilissimo trucco "struct che termina con un array di lunghezza 0" ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
error C2466: cannot allocate an array of constant size 0
In C ++ 11 i test SFINAE sono diventati molto più belli. Ecco alcuni esempi di usi comuni:
Scegli un sovraccarico di funzioni a seconda dei tratti
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 un cosiddetto idioma di tipo sink puoi fare test piuttosto arbitrari su un tipo come controllare se ha un membro e se quel membro è di un certo 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;
}
Ecco un esempio dal vivo: http://ideone.com/dHhyHE Recentemente ho anche scritto un'intera sezione su SFINAE e la spedizione di tag nel mio blog (plug spudorato ma pertinente) http://metaporky.blogspot.de/2014/08/ part-7-static-dispatch-function.html
Nota a partire da C ++ 14 c'è uno std :: void_t che è essenzialmente lo stesso del mio TypeSink qui.
TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>
in un posto e poi TypeSinkT<decltype(&T::bar)>
in un altro? Inoltre è il &
necessario in std::declval<T&>
?
TypeSink
, C ++ 17 ha std::void_t
:)
La libreria enable_if di Boost offre una bella interfaccia pulita per l'utilizzo di SFINAE. Uno dei miei esempi di utilizzo preferiti è nella libreria Boost.Iterator . SFINAE viene utilizzato per abilitare le conversioni del tipo di iteratore.
C ++ 17 probabilmente fornirà un mezzo generico per interrogare le funzionalità. Vedere N4502 per i dettagli, ma come esempio autonomo si consideri quanto segue.
Questa parte è la parte costante, inseriscila in un'intestazione.
// 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 {};
Il seguente esempio, tratto da N4502 , mostra l'utilizzo:
// 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>;
Rispetto alle altre implementazioni, questa è abbastanza semplice: è sufficiente un set ridotto di strumenti ( void_t
e detect
). Inoltre, è stato riportato (vedere N4502 ) che è misurabilmente più efficiente (tempo di compilazione e consumo di memoria del compilatore) rispetto agli approcci precedenti.
Ecco un esempio dal vivo , che include modifiche alla portabilità per GCC pre 5.1.
Ecco un altro (in ritardo) SFINAE esempio, sulla base di Greg Rogers 's risposta :
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);
In questo modo, puoi controllare il value
valore di per vedere se T
è una classe o meno:
int main(void) {
std::cout << IsClassT<std::string>::value << std::endl; // true
std::cout << IsClassT<int>::value << std::endl; // false
return 0;
}
int C::*
nella tua risposta? Come può C::*
essere un nome di parametro?
int C::*
è il tipo di puntatore a una int
variabile membro di C
.
Ecco un buon articolo di SFINAE: Un'introduzione al concetto SFINAE di C ++: introspezione in fase di compilazione di un membro della classe .
Riepilogalo come segue:
/*
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
è un'utilità che fornisce un "falso riferimento" a un oggetto di un tipo che non potrebbe essere facilmente costruito. declval
è davvero utile per le nostre costruzioni 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';
}
Qui, sto usando l'overload della funzione del modello (non direttamente SFINAE) per determinare se un puntatore è una funzione o un puntatore di classe membro: ( È possibile correggere i puntatori alla funzione membro iostream cout / cerr stampati come 1 o true? )
#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;
}
stampe
0. false
1. true
2. true
3. true
4. true
Così com'è, il codice potrebbe (a seconda del "buono" del compilatore) generare una chiamata in fase di esecuzione a una funzione che restituirà vero o falso. Se desideri forzare la is_function_pointer(var)
valutazione al tipo di compilazione (nessuna chiamata di funzione eseguita in fase di esecuzione), puoi utilizzare il constexpr
trucco della variabile:
constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;
Per lo standard C ++, constexpr
è garantito che tutte le variabili vengano valutate in fase di compilazione ( lunghezza di calcolo di una stringa C in fase di compilazione. È davvero un constexpr? ).
Il codice seguente utilizza SFINAE per consentire al compilatore di selezionare un overload in base al fatto che un tipo abbia o meno un determinato metodo:
#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;
}
Produzione:
Galleggiante: 1 Int: -1