Esempi di C ++ SFINAE?


122

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?


2
Questa è una buona domanda. Capisco SFINAE abbastanza bene, ma non credo di averlo mai dovuto usare (a meno che le biblioteche non lo facciano senza che io lo sappia).
Zifre,

5
STL lo ha messo in modo leggermente diverso nelle FAQ qui , "Il fallimento della sostituzione non è un elefante"
corvo vulcaniano

Risposte:


72

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>::Yesviene 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.


8
@rlbond, ho risposto alla tua domanda nei commenti a questa domanda qui: stackoverflow.com/questions/822059/… . In breve: se entrambe le funzioni di test sono candidate e valide, allora "..." ha il costo di conversione peggiore, e quindi non sarà mai preso, a favore dell'altra funzione. "..." sono i puntini di sospensione, cosa var-arg: int printf (char const *, ...);
Johannes Schaub - litb


20
La cosa più strana qui IMO non è il ..., 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/...
HostileFork dice fiducia Dont SE

1
qualcuno può spiegare cos'è C :: *? Ho letto tutti i commenti e collegamenti, ma mi chiedo ancora, int C :: * significa che è un puntatore membro di tipo int. cosa succede se una classe non ha membri di tipo int? Cosa mi manca? e come funziona test <T> (0) in questo? Devo essermi perso qualcosa
user2584960

92

Mi piace usare SFINAEper 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.


1
@Johannes Stranamente, GCC (4.8) e Clang (3.2) accettano di dichiarare array di dimensione 0 (quindi il tipo non è realmente "non valido"), ma si comporta correttamente sul tuo codice. Probabilmente c'è un supporto speciale per questo caso nel caso di SFINAE rispetto agli usi "regolari" dei tipi.
akim

@akim: se questo è mai vero (strano?! da quando?) allora forse M <= N ? 1 : -1potrebbe funzionare invece.
v.oddou

1
@ v.oddou Provaci 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 ).
akim

@akim: sì, è quello che pensavo -> C99. Questo non è consentito in C ++, ecco cosa ottieni con un compilatore moderno:error C2466: cannot allocate an array of constant size 0
v.oddou

1
@ v.oddou No, intendevo davvero C ++, e in realtà C ++ 11: sia clang ++ che g ++ lo accettano, e ho indicato una pagina che spiega perché è utile.
akim

16

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.


Il tuo primo blocco di codice ridefinisce lo stesso modello.
TC

Poiché non esiste alcun tipo per cui is_integral e is_floating_point siano entrambi veri, dovrebbe essere uno o perché SFINAE ne rimuoverà almeno uno.
venerdì

Stai ridefinendo lo stesso modello con diversi argomenti del modello predefinito. Hai provato a compilarlo?
TC

2
Sono nuovo nella metaprogrammazione dei modelli, quindi volevo capire questo esempio. C'è un motivo per cui usi 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&>?
Kevin Doyon

1
Riguardo al tuo TypeSink, C ++ 17 ha std::void_t:)
YSC

10

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.


4

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_te 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.


3

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

Cosa significa questa sintassi int C::*nella tua risposta? Come può C::*essere un nome di parametro?
Kirill Kobelev

1
È un puntatore al membro. Qualche riferimento: isocpp.org/wiki/faq/pointers-to-members
whoan

@KirillKobelev int C::*è il tipo di puntatore a una intvariabile membro di C.
YSC

3

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

0

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? )

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

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 constexprtrucco 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? ).


0

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.