iX 10/2019
S. 108
Wissen
Programmierung

Programmiertechnik SFINAE für Member Checker und bedingte Kompilierung anwenden

Hilfreiche Fehler

Detlef Wilkening

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 Template­parameter fehl, wird das Funk­tionstemplate 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 Si­gnatur) stellen bei einem Funktionsaufruf über den Namen mögliche Kandidaten für den eigentlichen Aufruf dar. Der Compiler entscheidet anhand der Argument­typen 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 Funktions­tem­plate:

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 Funktionstem­plate ein möglicher Kandidat für den Funk­tionsaufruf 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 Funk­tions­tem­plate 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 Funk­tionstemplate 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 Situa­tion nicht als Fehler zu behandeln. Wenn also bei der Substitution in der Schnitt­stelle während der Typdeduktion von Funk­tionstemplates 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.

Kommentieren