SFINAE
"Substitution Failure Is Not An Error"
Diese Regel gilt während der Überlagerungsauflösung von Funktionsvorlagen: Wenn
Substitution
des explizit spezifizierten oder
deduzierten Typs
für den Template-Parameter fehlschlägt, wird die Spezialisierung aus dem
Überlagerungssatz
verworfen, anstatt einen Kompilierfehler zu verursachen.
Diese Funktion wird in der Template-Metaprogrammierung verwendet.
Inhaltsverzeichnis |
Erklärung
Funktions-Template-Parameter werden zweimal substituiert (durch Template-Argumente ersetzt):
- explizit angegebene Template-Argumente werden vor der Template-Argument-Deduktion substituiert
- deduzierte Argumente und die von den Standardwerten erhaltenen Argumente werden nach der Template-Argument-Deduktion substituiert
Substitution erfolgt in
- alle in der Funktionstyp verwendeten Typen (was den Rückgabetyp und die Typen aller Parameter einschließt)
- alle in den Template-Parameterdeklarationen verwendeten Typen
- alle in der Template-Argumentliste einer partiellen Spezialisierung verwendeten Typen
|
(seit C++11) |
|
(seit C++20) |
Ein Substitution Failure liegt vor, wenn der Typ oder Ausdruck oben fehlerhaft wäre (mit erforderlicher Diagnose), wenn er mit den substituierten Argumenten geschrieben würde.
Nur die Fehler in den Typen und Ausdrücken im unmittelbaren Kontext des Funktionstyps oder seiner Template-Parameter-Typen oder seines explicit specifier (since C++20) sind SFINAE-Fehler. Wenn die Auswertung eines substituierten Typs/Ausdrucks einen Nebeneffekt wie die Instanziierung einer Template-Spezialisierung, die Generierung einer implizit definierten Member-Funktion etc. verursacht, werden Fehler in diesen Nebeneffekten als harte Fehler behandelt. Ein lambda expression wird nicht als Teil des unmittelbaren Kontexts betrachtet. (since C++20)
|
Dieser Abschnitt ist unvollständig
Grund: Mini-Beispiel, wo dies relevant ist |
Die Substitution erfolgt in lexikalischer Reihenfolge und stoppt, wenn ein Fehler auftritt.
|
Falls es mehrere Deklarationen mit unterschiedlicher lexikalischer Reihenfolge gibt (z.B. eine Funktionsvorlage, die mit nachgestellten Rückgabetyp deklariert wird, der nach einem Parameter substituiert wird, und mit gewöhnlichem Rückgabetyp neu deklariert wird, der vor dem Parameter substituiert werden würde), und dies dazu führen würde, dass Template-Instanziierungen in einer anderen Reihenfolge oder überhaupt nicht stattfinden, dann ist das Programm fehlerhaft; keine Diagnose erforderlich. |
(since C++11) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // SFINAE-Fehler, falls T kein Member type hat class V = typename B<T>::type> // harter Fehler, falls B kein Member type hat // (garantiert nicht auftretend via CWG 1227, da // Substitution in das Standard-Template-Argument // von U zuerst fehlschlagen würde) 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; // Neu-Deklaration template<class T> void h(...) {} using R = decltype(h<int>(0)); // fehlerhaft, keine Diagnose erforderlich
Type SFINAE
Die folgenden Typfehler sind SFINAE-Fehler:
|
(since 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-integraler Größe oder ein Array mit der Größe Null zu erstellen:
template<int I> void div(char(*)[I % 2 == 0] = nullptr) { // diese Überladung wird ausgewählt, wenn I gerade ist } template<int I> void div(char(*)[I % 2 == 1] = nullptr) { // diese Überladung wird ausgewählt, wenn I ungerade ist }
-
Versuch, einen Typ links vom Bereichsauflösungsoperator
::zu verwenden, und es ist kein Klasse oder Enumeration:
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // verwendet die zweite Überladung
- Versuch, ein Mitglied eines Typs zu verwenden, wo
-
- der Typ enthält nicht das angegebene Mitglied
- das angegebene Mitglied ist kein Typ, wo ein Typ erforderlich ist
- das angegebene Mitglied ist keine Vorlage, wo eine Vorlage erforderlich ist
- das angegebene Mitglied ist kein Nicht-Typ, 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() { // Ableitung schlägt in jedem dieser Fälle fehl: f<A>(0); // A enthält kein Mitglied Y f<B>(0); // Das Y-Mitglied von B ist kein Typ g<C>(0); // Das N-Mitglied von C ist kein Nicht-Typ h<D>(0); // Das TT-Mitglied von D ist kein Template // Ableitung gelingt in jedem dieser Fälle: f<B1>(0); g<C1>(0); h<D1>(0); } // todo: muss Überladungsauflösung demonstrieren, nicht nur Fehlschläge
- Versuch, einen Zeiger auf eine Referenz zu erstellen
- Versuch, eine Referenz auf void zu erstellen
- Versuch, einen Zeiger auf ein Mitglied 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::*); // wird ausgewählt, wenn C ein Klassentyp ist template<typename C> static no& test(...); // wird andernfalls ausgewählt public: static bool const value = sizeof(test<T>(nullptr)) == sizeof(yes); };
- Versuch, einem konstanten Template-Parameter 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: Muss Überladungsauflösung demonstrieren, nicht nur Fehler
- Versuch, eine ungültige Konvertierung durchzuführen
-
- in einem Template-Argumentausdruck
- in einem Ausdruck, der in einer Funktionsdeklaration verwendet wird:
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // kann 1 nicht in int* konvertieren // todo: muss Überladungsauflösung demonstrieren, nicht nur Fehler
- 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
Expression SFINAE
|
Vor C++11 mussten nur konstante Ausdrücke, die in Typen verwendet werden (wie Array-Grenzen), als SFINAE behandelt werden (und nicht als harte Fehler). |
(until 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 |
(since C++11) |
SFINAE in partiellen Spezialisierungen
Deduktion und Substitution erfolgen auch bei der Bestimmung, ob eine Spezialisierung einer Klassen- oder Variablen (since C++14) -Template durch eine partielle Spezialisierung oder das primäre Template erzeugt wird. Ein Substitutionsfehler wird während dieser Bestimmung nicht als schwerwiegender Fehler behandelt, sondern führt stattdessen dazu, dass die entsprechende partielle Spezialisierungsdeklaration ignoriert wird, ähnlich wie bei der Überladungsauflösung mit Funktions-Templates.
// Primäres Template behandelt nicht-referenzierbare Typen: template<class T, class = void> struct reference_traits { using add_lref = T; using add_rref = T; }; // Spezialisierung erkennt referenzierbare Typen: 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;
Bibliotheksunterstützung
|
Die Standardbibliothek-Komponente std::enable_if ermöglicht das Erzeugen eines Substitution Failure, um bestimmte Überladungen basierend auf einer zur Compilezeit ausgewerteten Bedingung zu aktivieren oder zu deaktivieren. Zusätzlich müssen viele Type Traits mit SFINAE implementiert werden, falls entsprechende Compiler-Erweiterungen nicht verfügbar sind. |
(since C++11) |
|
Die Standardbibliothek-Komponente std::void_t ist eine weitere Utility-Metafunktion, die partielle Spezialisierungs-SFINAE-Anwendungen vereinfacht. |
(since C++17) |
Alternativen
Wo anwendbar,
tag dispatch
,
if constexpr
(since C++17)
, und
concepts
(since C++20)
werden in der Regel der Verwendung von SFINAE vorgezogen.
|
|
(seit C++11) |
Beispiele
Ein gängiges Idiom ist die Verwendung von Expression SFINAE auf den Rückgabetyp, wobei der Ausdruck den Kommaoperator verwendet, dessen linker Teilausdruck derjenige ist, der überprüft wird (cast zu void, um sicherzustellen, dass kein benutzerdefinierter Kommaoperator für den Rückgabetyp ausgewählt wird), und dessen rechter Teilausdruck 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
Fehlerberichte
Die folgenden verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | Angewendet auf | Verhalten wie veröffentlicht | Korrektes Verhalten |
|---|---|---|---|
| CWG 295 | C++98 |
Erstellung von cv-qualifiziertem Funktionstyp
konnte zu Substitutionsfehler führen |
kein Fehler mehr,
cv-Qualifizierung wird verworfen |
| CWG 1227 | C++98 | die Reihenfolge der Substitution war nicht spezifiziert | identisch mit der lexikalischen Reihenfolge |
| CWG 2054 | C++98 | Substitution in partiellen Spezialisierungen war nicht korrekt spezifiziert | spezifiziert |
| CWG 2322 | C++11 |
Deklarationen in unterschiedlichen lexikalischen Reihenfolgen würden Template-
Instanziierungen in unterschiedlicher Reihenfolge oder gar nicht verursachen |
solcher Fall ist fehlerhaft,
keine Diagnose erforderlich |