C ++ SFINAE Beispiele?


122

Ich möchte mehr über die Meta-Programmierung von Vorlagen erfahren. Ich weiß, dass SFINAE für "Substitutionsfehler ist kein Fehler" steht. Aber kann mir jemand eine gute Verwendung für SFINAE zeigen?


2
Das ist eine gute Frage. Ich verstehe SFINAE ziemlich gut, aber ich glaube nicht, dass ich es jemals benutzen musste (es sei denn, Bibliotheken tun es, ohne dass ich es weiß).
Zifre

5
STL hat es in den FAQs hier etwas anders ausgedrückt : "Substitutionsfehler sind kein Elefant"
Vulkanrabe

Antworten:


72

Hier ist ein Beispiel ( von hier ):

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

Wenn IsClassT<int>::Yesausgewertet wird, kann 0 nicht konvertiert werden, int int::*da int keine Klasse ist und daher keinen Elementzeiger haben kann. Wenn SFINAE nicht vorhanden wäre, würde ein Compilerfehler angezeigt, z. B. "0 kann nicht in Mitgliedszeiger für Nicht-Klassentyp int konvertiert werden". Stattdessen wird nur das ...Formular verwendet, das Two zurückgibt und daher als false ausgewertet wird. Int ist kein Klassentyp.


8
@rlbond, ich habe Ihre Frage in den Kommentaren zu dieser Frage hier beantwortet: stackoverflow.com/questions/822059/… . Kurz gesagt: Wenn beide Testfunktionen Kandidaten sind und realisierbar sind, hat "..." die schlechtesten Konvertierungskosten und wird daher niemals zugunsten der anderen Funktion genommen. "..." ist das Auslassungszeichen, var-arg: int printf (char const *, ...);
Johannes Schaub - Litb


20
Das Seltsamere hier, IMO, ist nicht das ..., sondern das int C::*, was ich noch nie gesehen hatte und nachschlagen musste. Hier finden Sie die Antwort darauf, was das ist und wofür es verwendet werden könnte: stackoverflow.com/questions/670734/…
HostileFork sagt, dass Sie SE

1
kann jemand erklären, was C :: * ist? Ich habe alle Kommentare und Links gelesen, frage mich aber immer noch, int C :: * bedeutet, dass es sich um einen Mitgliedszeiger vom Typ int handelt. Was ist, wenn eine Klasse kein Mitglied vom Typ int hat? Was vermisse ich? und wie spielt test <T> (0) dazu? Ich muss etwas vermissen
user2584960

92

Ich benutze gerne SFINAE, um boolesche Bedingungen zu überprüfen.

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 */
}

Es kann sehr nützlich sein. Zum Beispiel habe ich damit überprüft, ob eine mit Operator Komma gesammelte Initialisiererliste nicht länger als eine feste Größe ist

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

Die Liste wird nur akzeptiert, wenn M kleiner als N ist, was bedeutet, dass die Initialisierungsliste nicht zu viele Elemente enthält.

Die Syntax char(*)[C]bedeutet: Zeiger auf ein Array mit dem Elementtyp char und size C. Wenn Cfalse (hier 0) ist, erhalten wir den ungültigen char(*)[0]Zeiger auf ein Array mit der Größe Null: SFINAE macht es so, dass die Vorlage dann ignoriert wird.

Ausgedrückt mit boost::enable_if, das sieht so aus

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

In der Praxis finde ich die Fähigkeit, Bedingungen zu überprüfen, oft eine nützliche Fähigkeit.


1
@Johannes Seltsamerweise akzeptieren GCC (4.8) und Clang (3.2), Arrays der Größe 0 zu deklarieren (der Typ ist also nicht wirklich "ungültig"), aber er verhält sich in Ihrem Code ordnungsgemäß. Es gibt wahrscheinlich eine besondere Unterstützung für diesen Fall im Fall von SFINAE im Vergleich zu "regulären" Verwendungen von Typen.
Akim

@akim: Wenn das jemals wahr ist (komisch ?! Seit wann?), M <= N ? 1 : -1könnte es vielleicht stattdessen funktionieren.
v.oddou

1
@ v.oddou Versuch es einfach int foo[0]. Ich bin nicht überrascht, dass es unterstützt wird, da es den sehr nützlichen Trick "Struktur endet mit einem Array mit 0 Längen" ermöglicht ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
Akim

@akim: Ja, das habe ich mir gedacht -> C99. Dies ist in C ++ nicht erlaubt, hier ist, was Sie mit einem modernen Compiler bekommen:error C2466: cannot allocate an array of constant size 0
v.oddou

1
@ v.oddou Nein, ich meinte wirklich C ++ und eigentlich C ++ 11: sowohl clang ++ als auch g ++ akzeptieren es und ich habe auf eine Seite verwiesen, die erklärt, warum dies nützlich ist.
Akim

16

In C ++ 11 sind SFINAE-Tests viel schöner geworden. Hier sind einige Beispiele für häufige Verwendungen:

Wählen Sie abhängig von den Merkmalen eine Funktionsüberladung

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
}

Mit einem so genannten Typ-Sink-Idiom können Sie ziemlich willkürliche Tests für einen Typ durchführen, z. B. prüfen, ob er ein Mitglied hat und ob dieses Mitglied von einem bestimmten Typ ist

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

Hier ist ein Live-Beispiel: http://ideone.com/dHhyHE Ich habe kürzlich in meinem Blog einen ganzen Abschnitt über SFINAE und Tag-Versand geschrieben (schamloser Plug, aber relevant). Http://metaporky.blogspot.de/2014/08/ part-7-static-dispatch-function.html

Beachten Sie, dass es ab C ++ 14 ein std :: void_t gibt, das im Wesentlichen mit meinem TypeSink hier identisch ist.


Ihr erster Codeblock definiert dieselbe Vorlage neu.
TC

Da es keinen Typ gibt, für den is_integral und is_floating_point beide wahr sind, sollte es entweder oder sein, da SFINAE mindestens einen entfernt.
Odinthenerd

Sie definieren dieselbe Vorlage mit verschiedenen Standardvorlagenargumenten neu. Haben Sie versucht, es zu kompilieren?
TC

2
Ich bin neu in der Vorlagen-Metaprogrammierung, daher wollte ich dieses Beispiel verstehen. Gibt es einen Grund, den Sie TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>an einem Ort und dann TypeSinkT<decltype(&T::bar)>an einem anderen verwenden? Auch ist das &notwendig in std::declval<T&>?
Kevin Doyon

1
Über Ihre TypeSink, C ++ 17 haben std::void_t:)
YSC

10

Die enable_if- Bibliothek von Boost bietet eine schöne, saubere Oberfläche für die Verwendung von SFINAE. Eines meiner bevorzugten Anwendungsbeispiele befindet sich in der Boost.Iterator- Bibliothek. SFINAE wird verwendet, um Konvertierungen vom Iteratortyp zu aktivieren.


4

C ++ 17 bietet wahrscheinlich ein generisches Mittel zum Abfragen von Features. Siehe N4502 für Details, aber als eigenständiges Beispiel betrachten Sie Folgendes.

Dieser Teil ist der konstante Teil, setzen Sie ihn in eine Kopfzeile.

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

Das folgende Beispiel aus N4502 zeigt die Verwendung:

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

Im Vergleich zu den anderen Implementierungen ist diese ziemlich einfach: Ein reduzierter Satz von Werkzeugen ( void_tund detect) reicht aus. Außerdem wurde berichtet (siehe N4502 ), dass es messbar effizienter ist (Kompilierungszeit und Compiler-Speicherverbrauch) als frühere Ansätze.

Hier ist ein Live-Beispiel , das Portabilitätsverbesserungen für GCC vor 5.1 enthält.


3

Hier ist ein weiteres (spätes) SFINAE- Beispiel, das auf Greg Rogers ' Antwort basiert :

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

Auf diese Weise können Sie den valueWert des 's überprüfen , um festzustellen, ob Tes sich um eine Klasse handelt oder nicht:

int main(void) {
    std::cout << IsClassT<std::string>::value << std::endl; // true
    std::cout << IsClassT<int>::value << std::endl;         // false
    return 0;
}

Was bedeutet diese Syntax int C::*in Ihrer Antwort? Wie kann C::*ein Parametername sein?
Kirill Kobelev

1
Es ist ein Zeiger auf das Mitglied. Einige Referenzen: isocpp.org/wiki/faq/pointers-to-members
whoan

@KirillKobelev int C::*ist der Typ eines Zeigers auf eine intMitgliedsvariable von C.
YSC

3

Hier ist ein guter Artikel von SFINAE: Eine Einführung in das SFINAE-Konzept von C ++: Introspektion eines Klassenmitglieds zur Kompilierungszeit .

Fassen Sie es wie folgt zusammen:

/*
 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);
}

declvalist ein Dienstprogramm, mit dem Sie einen "falschen Verweis" auf ein Objekt eines Typs erhalten, der nicht einfach zu erstellen ist. declvalist sehr praktisch für unsere SFINAE-Konstruktionen.

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

Hier verwende ich die Überladung von Vorlagenfunktionen (nicht direkt SFINAE), um zu bestimmen, ob ein Zeiger eine Funktion oder ein Elementklassenzeiger ist: ( Ist es möglich, die iostream cout / cerr-Elementfunktionszeiger zu korrigieren, die als 1 oder true gedruckt werden? )

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

Druckt

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

So wie der Code ist, könnte er (abhängig vom "guten" Willen des Compilers) einen Laufzeitaufruf für eine Funktion erzeugen, die true oder false zurückgibt. Wenn Sie die is_function_pointer(var)Auswertung beim Kompilieren erzwingen möchten (zur Laufzeit werden keine Funktionsaufrufe ausgeführt), können Sie den constexprVariablentrick verwenden:

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

Nach dem C ++ - Standard wird constexprgarantiert, dass alle Variablen zur Kompilierungszeit ausgewertet werden ( Berechnen der Länge eines C-Strings zur Kompilierungszeit. Ist dies wirklich ein Nachteil? ).


0

Der folgende Code verwendet SFINAE, damit der Compiler eine Überladung basierend darauf auswählen kann, ob ein Typ eine bestimmte Methode hat oder nicht:

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

Ausgabe:

Float: 1
Int: -1
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.