Programmiertechnik SFINAE für Member Checker und bedingte Kompilierung anwenden
Hilfreiche Fehler
Das Ersetzen von Typparametern führt oft zu vermeintlichen Kompilierfehlern. Mit Techniken wie SFINAE lassen diese sich für nützliche Zwecke einsetzen.
SFINAE ist ein in der C++-Welt verbreitetes Akronym, das David Vandevoorde und Nicolai Josuttis 2002 in ihrem Buch „C++ Templates“ prägten. Es steht für „Substitution failure is not an error“ und bezeichnet eine Regel, die während der Überladungsauflösung von Funktionstemplates gilt. Schlägt das Ersetzen eines explizit angegebenen oder abgeleiteten Typs in der Signatur für den Templateparameter fehl, wird das Funktionstemplate aus dem Überladungssatz entfernt, anstatt einen Kompilierfehler zu verursachen.
Aktuelle und kommende C++-Standards versuchen mit std::enable_if
(C++11) oder std::void_t
(C++17) die Nutzung von SFINAE zu vereinfachen oder mit constexpr if
(C++17) oder Concepts (C++20) einfacher zu nutzende Alternativen zu SFINAE anzubieten. Trotzdem ist der Einsatz von SFINAE noch immer aktuell. C++11 führte Expression SFINAE ein und in C++20 wird SFINAE auf den Explicit Specifier erweitert. Hintergrund ist, dass SFINAE in der Metaprogrammierung immer noch eine notwendige Technik ist, die meisten Type Traits in C++ damit implementiert sind und die Range-Bibliothek in C++20 ausgiebigen Gebrauch davon macht.
Zum Verstehen von Templatemechanismen dient folgendes Beispiel: Überladene Funktionen (tragen den gleichen Namen und unterscheiden sich in der Signatur) stellen bei einem Funktionsaufruf über den Namen mögliche Kandidaten für den eigentlichen Aufruf dar. Der Compiler entscheidet anhand der Argumenttypen und Signaturen, welche Funktion am besten passt, und ruft sie auf:
void fct(int); void fct(double); void fct(const string&); fct(42); // Alle Funktionen sind vom // Namen her moeglich // Aufruf von "fct(int)" // wegen des Int-Arguments
Im Beispiel ist „42“ ein Int-Literal und es gibt eine Funktion fct
, die int
erwartet – die Zuordnung ist einfach und eindeutig. Die Menge an möglichen Funktionen erhält noch mindestens ein Funktionstemplate:
void fct(int); void fct(double); void fct(const string&); template<class T> void fct(T); fct(42); // => fct(int) fct(true); // => Funktionstemplate mit // T==bool => fct<bool>(bool)
Der Compiler erkennt bei beiden fct()
-Aufrufen, dass auch das Funktionstemplate ein möglicher Kandidat für den Funktionsaufruf ist, und deduziert in beiden Fällen den Templatetypparameter T. Für die Deduktion versucht der Compiler, den Argumenttyp auf den Parametertyp zu matchen und dadurch das „T“ zu deduzieren. Im ersten Fall erkennt er T als int
und im zweiten Fall als bool
.
Da es im ersten Fall eine exakt passende normale Funktion gibt, wählt der Compiler diese aus. Im zweiten Fall passen durch die möglichen Typkonvertierungen auch die Int- und die Double-Funktion. Aber das Funktionstemplate matcht besser, weshalb der Compiler dieses wählt.
Der Code erhält ein weiteres Funktionstemplate mit dem komplizierteren Parameter T::type
. Dieser Parameter benötigt zusätzlich das Schlüsselwort typename
vor dem Namen, da T::type
ein abhängiger Name ist und der Compiler die Information braucht, dass T::type
ein Typ ist (Listing 1; alle Listings siehe ix.de/zfu8).
Bei näherer Betrachtung der Typdeduktion für den zweiten Fall (der erste ist analog) mit fct(true)
zeigt sich, dass der Compiler wie gehabt im ersten Funktionstemplate T als bool
deduziert. Beim zweiten Funktionstemplate ergibt sich für ihn aber ein Problem: Der Typ bool
matcht nicht mit dem Parameter T::type
– es gibt keine sinnvolle Substitution. Das sollte eigentlich einen Compilerfehler erzeugen. Aber der „Bibliothek“ wurde nur ein neues Funktionstemplate hinzugefügt, der bestehende Code bricht jedoch und kompiliert nicht mehr. Deshalb beschloss das C++-Standardisierungskomitee, diese Situation nicht als Fehler zu behandeln. Wenn also bei der Substitution in der Schnittstelle während der Typdeduktion von Funktionstemplates ein Fehler auftritt, dann ignoriert der Compiler diesen still („Substitution failure is not an error“). Das entsprechende Funktionstemplate wird aus der Menge der möglichen Aufrufkandidaten herausgenommen.
Der erste C++-Standard von 1998 (ISO/IEC 14882:1998(E)) beschreibt bereits dieses Verhalten (Paragraf 14.8). Programmierer haben zwischenzeitlich Ideen entwickelt, es als Feature einzusetzen. Deshalb forderte die Community einen Namen dafür, den Vandevoorde/Josuttis 2002 mit SFINAE ins Spiel brachten.