SFINAE
"Substitution Failure Is Not An Error"
Diese Regel gilt während der Überladungsauflösung von Funktionsvorlagen: Wenn das Ersetzen des explizit angegebenen oder abgeleiteten Typs für den Vorlagenparameter fehlschlägt, wird die Spezialisierung aus der Überladungsmenge verworfen, anstatt einen Kompilierungsfehler zu verursachen.
Dieses Feature wird in der Template-Metaprogrammierung verwendet.
Inhalt |
[edit] Erklärung
Funktionsvorlagenparameter werden zweimal ersetzt (durch Vorlagenargumente)
- explizit angegebene Vorlagenargumente werden vor der Vorlagenargumentableitung ersetzt
- abgeleitete Argumente und aus Standardwerten gewonnene Argumente werden nach der Vorlagenargumentableitung ersetzt
Ersetzung findet statt in
- allen Typen, die im Funktionstyp verwendet werden (einschließlich Rückgabetyp und Typen aller Parameter)
- allen Typen, die in den Vorlagenparametrisierungsklauseln verwendet werden
- allen Typen, die in der Liste der Vorlagenargumente einer partiellen Spezialisierung verwendet werden
|
(seit C++11) |
|
(seit C++20) |
Ein Substitutionsfehler ist jede Situation, in der der obige Typ oder Ausdruck fehlerhaft wäre (mit einer erforderlichen Diagnose), wenn er mit den ersetzten Argumenten geschrieben würde.
Nur Fehler in den Typen und Ausdrücken im unmittelbaren Kontext des Funktionstyps oder seiner Vorlagenparametertypen oder seines expliziten Spezifizierers(seit C++20) sind SFINAE-Fehler. Wenn die Auswertung eines ersetzten Typs/Ausdrucks Nebeneffekte wie die Instanziierung einer Vorlagenspezialisierung, die Generierung einer implizit definierten Member-Funktion usw. verursacht, werden Fehler in diesen Nebeneffekten als harte Fehler behandelt. Ein Lambda-Ausdruck wird nicht als Teil des unmittelbaren Kontexts betrachtet.(seit C++20)
| Dieser Abschnitt ist unvollständig Grund: Mini-Beispiel, wo das relevant ist |
Die Ersetzung erfolgt in lexikalischer Reihenfolge und stoppt, wenn ein Fehler auftritt.
|
Wenn es mehrere Deklarationen mit unterschiedlichen lexikalischen Ordnungen gibt (z. B. eine Funktionsvorlage, die mit einem nachgestellten Rückgabetyp deklariert wird, um nach einem Parameter ersetzt zu werden, und mit einem gewöhnlichen Rückgabetyp neu deklariert wird, der vor dem Parameter ersetzt werden würde), und dies dazu führen würde, dass Vorlageninstanziierungen in einer anderen Reihenfolge oder gar nicht auftreten, dann ist das Programm fehlerhaft; keine Diagnose erforderlich. |
(seit C++11) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // SFINAE failure if T has no member type class V = typename B<T>::type> // hard error if B has no member type // (guaranteed to not occur via CWG 1227 because // substitution into the default template argument // of U would fail first) void foo (int); template<class T> typename T::type h(typename B<T>::type); template<class T> auto h(typename B<T>::type) -> typename T::type; // redeclaration template<class T> void h(...) {} using R = decltype(h<int>(0)); // ill-formed, no diagnostic required
[edit] Type SFINAE
Die folgenden Typfehler sind SFINAE-Fehler
|
(seit C++11) |
- Versuch, ein Array von void, ein Array von Referenzen, ein Array von Funktionen, ein Array mit negativer Größe, ein Array mit nicht-ganzzahliger Größe oder ein Array der Größe Null zu erstellen
template<int I> void div(char(*)[I % 2 == 0] = nullptr) { // this overload is selected when I is even } template<int I> void div(char(*)[I % 2 == 1] = nullptr) { // this overload is selected when I is odd }
- Versuch, einen Typ links von einem Scope-Auflösungsoperator `::` zu verwenden, und dieser ist keine Klasse oder Aufzählung
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // uses second overload
- Versuch, ein Member eines Typs zu verwenden, wobei
- der Typ den angegebenen Member nicht enthält
- der angegebene Member kein Typ ist, wo ein Typ erforderlich ist
- der angegebene Member keine Vorlage ist, wo eine Vorlage erforderlich ist
- der angegebene Member kein Nicht-Typ ist, wo ein Nicht-Typ erforderlich ist
template<int I> struct X {}; template<template<class T> class> struct Z {}; template<class T> void f(typename T::Y*) {} template<class T> void g(X<T::N>*) {} template<class T> void h(Z<T::template TT>*) {} struct A {}; struct B { int Y; }; struct C { typedef int N; }; struct D { typedef int TT; }; struct B1 { typedef int Y; }; struct C1 { static const int N = 0; }; struct D1 { template<typename T> struct TT {}; }; int main() { // Deduction fails in each of these cases: f<A>(0); // A does not contain a member Y f<B>(0); // The Y member of B is not a type g<C>(0); // The N member of C is not a non-type h<D>(0); // The TT member of D is not a template // Deduction succeeds in each of these cases: f<B1>(0); g<C1>(0); h<D1>(0); } // todo: needs to demonstrate overload resolution, not just failure
- Versuch, einen Zeiger auf eine Referenz zu erstellen
- Versuch, eine Referenz auf void zu erstellen
- Versuch, einen Zeiger auf ein Member von T zu erstellen, wobei T kein Klassentyp ist
template<typename T> class is_class { typedef char yes[1]; typedef char no[2]; template<typename C> static yes& test(int C::*); // selected if C is a class type template<typename C> static no& test(...); // selected otherwise public: static bool const value = sizeof(test<T>(nullptr)) == sizeof(yes); };
- Versuch, einem Nicht-Typ-Vorlagenparameter einen ungültigen Typ zuzuweisen
template<class T, T> struct S {}; template<class T> int f(S<T, T()>*); struct X {}; int i0 = f<X>(0); // todo: needs to demonstrate overload resolution, not just failure
- Versuch, eine ungültige Konvertierung durchzuführen in
- in einem Vorlagenargumentausdruck
- in einem im Funktionsdeklaration verwendeten Ausdruck
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // can’t conv 1 to int* // todo: needs to demonstrate overload resolution, not just failure
- Versuch, einen Funktionstyp mit einem Parameter vom Typ void zu erstellen
- Versuch, einen Funktionstyp zu erstellen, der einen Array-Typ oder einen Funktionstyp zurückgibt
[edit] Expression SFINAE
|
Nur konstante Ausdrücke, die in Typen (wie Array-Grenzen) verwendet wurden, mussten vor C++11 als SFINAE (und nicht als harte Fehler) behandelt werden. |
(bis C++11) |
|
Die folgenden Ausdrucksfehler sind SFINAE-Fehler
struct X {}; struct Y { Y(X){} }; // X is convertible to Y template<class T> auto f(T t1, T t2) -> decltype(t1 + t2); // overload #1 X f(Y, Y); // overload #2 X x1, x2; X x3 = f(x1, x2); // deduction fails on #1 (expression x1 + x2 is ill-formed) // only #2 is in the overload set, and is called |
(seit C++11) |
[edit] SFINAE in partiellen Spezialisierungen
Ableitung und Ersetzung erfolgen auch bei der Bestimmung, ob eine Spezialisierung einer Klassen- oder Variablen-(seit C++14) Vorlage durch eine partielle Spezialisierung oder die Primärvorlage generiert wird. Ein Substitutionsfehler wird bei einer solchen Bestimmung nicht als harter Fehler behandelt, sondern macht die entsprechende partielle Spezialisierungsdeklaration stattdessen ignoriert, als ob bei der Überladungsauflösung von Funktionsvorlagen.
// primary template handles non-referenceable types: template<class T, class = void> struct reference_traits { using add_lref = T; using add_rref = T; }; // specialization recognizes referenceable types: template<class T> struct reference_traits<T, std::void_t<T&>> { using add_lref = T&; using add_rref = T&&; }; template<class T> using add_lvalue_reference_t = typename reference_traits<T>::add_lref; template<class T> using add_rvalue_reference_t = typename reference_traits<T>::add_rref;
[edit] Bibliotheksunterstützung
|
Die Standardbibliothekskomponente std::enable_if ermöglicht die Erzeugung eines Substitutionsfehlers, um bestimmte Überladungen basierend auf einer zur Kompilierungszeit ausgewerteten Bedingung zu aktivieren oder zu deaktivieren. Darüber hinaus müssen viele Typ-Traits mit SFINAE implementiert werden, wenn geeignete Compiler-Erweiterungen nicht verfügbar sind. |
(seit C++11) |
|
Die Standardbibliothekskomponente std::void_t ist eine weitere Utility-Metafunktion, die partielle Spezialisierungsanwendungen von SFINAE vereinfacht. |
(seit C++17) |
[edit] Alternativen
Wo anwendbar, werden Tag-Dispatch, if constexpr(seit C++17) und Konzepte(seit C++20) normalerweise der Verwendung von SFINAE vorgezogen.
|
|
(seit C++11) |
[edit] Beispiele
Ein gängiges Idiom ist die Verwendung von Expression SFINAE für den Rückgabetyp, wobei der Ausdruck den Komma-Operator verwendet, dessen linke Unterausdruck derjenige ist, der untersucht wird (auf void gecastet, um sicherzustellen, dass der benutzerdefinierte Komma-Operator des Rückgabetyps nicht ausgewählt wird), und der rechte Unterausdruck den Typ hat, den die Funktion zurückgeben soll.
#include <iostream> // This overload is added to the set of overloads if C is // a class or reference-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)(c.*f)(), void()) { std::cout << "(1) Class/class reference overload called\n"; } // This overload is added to the set of overloads if C is a // pointer-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)((c->*f)()), void()) { std::cout << "(2) Pointer overload called\n"; } // This overload is always in the set of overloads: ellipsis // parameter has the lowest ranking for overload resolution void test(...) { std::cout << "(3) Catch-all overload called\n"; } int main() { struct X { void f() {} }; X x; X& rx = x; test(x, &X::f); // (1) test(rx, &X::f); // (1), creates a copy of x test(&x, &X::f); // (2) test(42, 1337); // (3) }
Ausgabe
(1) Class/class reference overload called (1) Class/class reference overload called (2) Pointer overload called (3) Catch-all overload called
[edit] Fehlerberichte
Die folgenden Verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | angewendet auf | Verhalten wie veröffentlicht | Korrigiertes Verhalten |
|---|---|---|---|
| CWG 295 | C++98 | Erstellung eines cv-qualifizierten Funktionstyps kann zu Substitutionsfehler führen |
wird nicht als Fehler behandelt, indem cv-Qualifizierung verworfen wird |
| CWG 1227 | C++98 | die Reihenfolge der Ersetzung war nicht spezifiziert | dasselbe wie die lexikalische Reihenfolge |
| CWG 2054 | C++98 | Ersetzung in partiellen Spezialisierungen war nicht korrekt spezifiziert | spezifiziert |
| CWG 2322 | C++11 | Deklarationen in unterschiedlichen lexikalischen Ordnungen würden Vorlagen verursachen Instanziierungen treten in anderer Reihenfolge oder gar nicht auf |
ein solcher Fall ist fehlerhaft, keine Diagnose erforderlich |