Weiß ein Software-Entwickler die Intelligenz des Compilers zu nutzen, spart er sich das aufwändige und fehlerträchtige Prüfen der Software. Das übernehmen “static_assert” und die Type-Traits-Bibliothek.
Zehn Regeln sollen helfen, besseren Code in modernem C++ zu schreiben, mittlerweile erreicht diese Artikelreihe bereits den achten Halt (Abbildung 1). Der rät: “Lasse dir helfen.” Tatsächlich gibt es viele Komponenten im C++-Ökosystem, dank denen Entwickler deutlich besseren Code schreiben.
Das beginnt bei IDEs wie Eclipse [1] oder Visual Studio [2], setzt sich über dynamische und statische Code-Analysewerkzeuge wie Thread Sanitzer [3] oder Cpp Mem [4] fort, geht weiter mit Smart Pointern [5] oder Type Traits [6] und endet mit der C++-Kernsprache. Letztere kennt auch ein Feature wie die automatische Typ-Bestimmung mit »auto« [7] oder »static_assert« [8].
Der Artikel konzentriert sich auf das Traumpaar Type Traits und »static_assert«. Während »static_assert« es genehmigt, an den Quellcode gestellte Bedingungen während des Kompilierens zu überprüfen, erlaubt es die Type-Traits-Bibliothek, diese Bedingungen zu formulieren. Genau die richtigen Bausteine also, um das aktuelle Motto umzusetzen. Sie sollen einen Algorithmus, der den größten gemeinsamen Teiler (ggT) zweier Zahlen ermittelt, typsicher machen.
Der Schnelldurchlauf
Zunächst kann ein kurzer Blick auf »static_assert« und die Type-Traits-Bibliothek das Gedächtnis auffrischen. Der einfache Fall ist »static_assert«, das lediglich nach einem Ausdruck und einem Text verlangt. Der Ausdruck muss zur Übersetzungszeit einen Wahrheitswert zurückgeben. Liefert er ein »false«, zeigt der Compiler den vorgefertigten Text als Fehlermeldung und erzeugt als Konsequenz keine ausführbare Datei.
Auch erwähnenswert ist, dass der Compiler »static_assert« zur Übersetzungszeit auswertet. Daher entsteht keine Laufzeiteinschränkung. Zudem lässt sich »static_assert« in allen Bereichen des Programms anwenden. Der Nutzen von »static_assert« steht und fällt aber mit den Bedingungen, die ein Entwickler beim Kompilieren an den Quellcode stellen kann. Hier kommt die Type-Traits-Bibliothek ins Spiel. Sie erlaubt es, zur Übersetzungszeit Typ-Eigenschaften zu prüfen, Typ-Vergleiche und sogar -Änderungen vorzunehmen.
Wem dieser Schnelldurchlauf nicht genügt, der kann sich in der Online-Dokumentation auf http://cppreference.com tiefgehend über die »static_assert«-Funktion [8] und die Type-Traits-Bibliothek [6] informieren.
Ein erster, naiver Versuch
Der generische Algorithmus in Listing 1 ist nur eine Fingerübung. Er heißt »gcd« und ermittelt den größten gemeinsamen Teiler zweier Zahlen. Das Funktions-Template »gcd()« in den Zeilen 3 bis 9 setzt den bekannten euklidischen Algorithmus [9] ein, um den ggT zweier Zahlen zu berechnen. Das Programm ist schnell übersetzt und ausgeführt und liefert die erwartete Ausgabe (Abbildung 2).
Listing 1
Größter gemeinsamer Teiler (naiv)
01 #include <iostream>
02
03 template<typename T>
04 T gcd(T a, T b){
05 if( b == 0 ){ return a; }
06 else{
07 return gcd(b, a % b);
08 }
09 }
10
11 int main(){
12
13 std::cout << std::endl;
14
15 std::cout << "gcd(100, 10)= " << gcd(100, 10) << std::endl;
16 std::cout << "gcd(100, 33)= " << gcd(100, 33) << std::endl;
17 std::cout << "gcd(100, 0)= " << gcd(100, 0) << std::endl;
18 // std::cout << "gcd(3.5, 4.0)= " << gcd(3.5, 4.0) << std::endl;
19 // std::cout << "gcd(100, 10L)= " << gcd(100, 10L) << std::endl;
20
21 std::cout << std::endl;
22
23 }
Wer nun aber die auskommentierten Zeilen aktiviert, erkennt schnell die Schwächen der Implementierung des ggT-Algorithmus. Er lässt sich nicht nur für jeden beliebigen Datentyp und damit auch für »double«-Werte (Zeile 18), sondern auch für zwei verschiedene Datentypen aufrufen (Zeile 19). Der Compiler quittiert dies mit der geschwätzigen Fehlermeldung in Abbildung 3.

Abbildung 2: Die erste, naive Implementierung des ggT-Algorithmus scheint zunächst zu funktionieren.
Die Meldung liefert immerhin einige Anhaltspunkte. Die erste Zeile lautet »no matching function for call to ‘gcd(int, long int)’«. Das bedeutet, dass der Compiler für das erste Argument des Aufrufs »gcd(100, 10L)« den Typ »int« bestimmt, für das zweite den Typ »long int«. Das kann nicht gut gehen, da das Klassen-Template »gcd« nur einen Typ-Parameter »<T>« besitzt. Der kann nicht zugleich »int« und »long int« sein.
In eine ganz andere Richtung geht die letzte Zeile der Fehlermeldung. Der Compiler moniert, dass er den Modulo-Operator »%« nicht auf »double«-Werte anwenden kann. Recht hat er. Während der Entwickler dieses Problem mit Hilfe der Type-Traits-Bibliothek zügig behebt, verlangt ihm die Instanzierung des Funktions-Template mit den ähnlichen Ganzzahlen »gcd(10, 100L)« schon mehr Aufwand ab. Sie soll als richtiges Ergebnis »10« liefern.
Du darfst hier nicht rein
Die Type-Traits-Bibliothek enthält unter anderem die Funktion »std::is_integral <type>::value« [10]. Sie prüft zur Übersetzungszeit, ob »type« ein integraler Typ ist. Das ist genau das fehlende Puzzleteil, um den ggT-Algorithmus in Listing 2 typsicherer zu implementieren. Das Übersetzen des Programms scheitert mit einer einfachen Fehlermeldung (Abbildung 4), besser kann es nicht laufen.
Listing 2
Ist type ein integraler Typ?
01 #include <iostream>
02 #include <type_traits>
03
04 template<typename T>
05 T gcd(T a, T b){
06 static_assert(std::is_integral<T>::value, "T should be an integral type!");
07 if( b == 0 ){ return a; }
08 else{
09 return gcd(b, a % b);
10 }
11 }
12
13 int main(){
14
15 std::cout << std::endl;
16
17 std::cout << gcd(3.5, 4.0)<< std::endl;
18
19 std::cout << std::endl;
20
21 }
Das war schon vielversprechend, doch in der nächsten Optimierungsstufe lernt der ggT-Algorithmus, mit zwei verschiedenen integralen Typen umzugehen. Um das zu erreichen, ist ein Umbau des ggT-Algorithmus nötig, der auch das klassische Problem von Funktions-Templates berücksichtigt, die mehr als einen Typ annehmen.
Stellt sich die Frage nach dem Rückgabetyp für den ggT-Algorithmus, die Listing 3 mit drei Fragezeichen andeutet. Soll der Rückgabetyp »<T>R« aus dem größeren der beiden Template-Parameter »T1« und »T2«, dem kleineren oder einem neuen Typ bestehen, der sich aus beiden Template-Parametern zusammensetzt? Die Frage lässt sich leider nicht allgemein beantworten. Aber die Lösung des Problems liefert einmal mehr die Type-Traits-Bibliothek. Listing 4 stellt gleich zwei Lösungen vor.
Listing 3
Welcher Rückgabetyp für den ggT-Algorithmus?
01 template<typename T1, typename T2>
02 ??? gcdConditional(T1 a, T2 b){
03 static_assert(std::is_integral<T1>::value, "T1 should be an integral type!");
04 static_assert(std::is_integral<T2>::value, "T2 should be an integral type!");
05 if( b == 0 ){ return a; }
06 else{
07 return gcdConditional(b, a % b);
08 }
09 }
Listing 4
Rückgabetyp bestimmen
01 #include <iostream>
02 #include <type_traits>
03 #include <typeinfo>
04
05 template<typename T1, typename T2,
06 typename R = typename std::conditional <(sizeof(T1) < sizeof(T2)), T1, T2>::type>
07 R gcdConditional(T1 a, T2 b){
08 static_assert(std::is_integral<T1>::value, "T1 should be an integral type!");
09 static_assert(std::is_integral<T2>::value, "T2 should be an integral type!");
10 if( b == 0 ){ return a; }
11 else{
12 return gcdConditional(b, a % b);
13 }
14 }
15
16
17 template<typename T1, typename T2,
18 typename R = typename std::common_type<T1, T2>::type>
19 R gcdCommon(T1 a, T2 b){
20 static_assert(std::is_integral<T1>::value, "T1 should be an integral type!");
21 static_assert(std::is_integral<T2>::value, "T2 should be an integral type!");
22 if( b == 0 ){ return a; }
23 else{
24 return gcdCommon(b, a % b);
25 }
26 }
27
28 int main(){
29
30
31 std::cout << std::endl;
32
33 std::cout << "gcdConditional(100, 10LL) = " << gcdConditional(100, 10LL) << std::endl;
34 std::cout << "gcdCommon(100, 10LL) = " << gcdCommon(100, 10LL) << std::endl;
35
36 std::conditional <(sizeof(int) < sizeof(long long)), int, long long>::type gcd1 = gcdConditional(100, 10LL);
37 auto gcd2 = gcdCommon(100, 10LL);
38
39 std::cout << std::endl;
40
41 std::cout << "typeid(gcd1).name() = " << typeid(gcd1).name() << std::endl;
42 std::cout << "typeid(gcd2).name() = " << typeid(gcd2).name() << std::endl;
43
44 std::cout << std::endl;
45
46 }
Der Einfachheit halber heißen die Algorithmen in dem Beispiel »gcdConditional()« (Zeilen 5 bis 14) und »gcdCommon()« (Zeilen 17 bis 26). Beide Funktions-Templates besitzen einen Standardtyp für die Rückgabe. Im Falle des Funktions-Template »gcdConditional()« heißt dieser:
std::conditional <(sizeof(T1) < sizeof(T2)), T1, T2>::type
Im Falle des Funktions-Template »gcdCommon()« lautet der Typ:
std::common_type<T1, T2>::type
Während das erste Beispiel eine Art ternären Operator zeigt, der zur Übersetzungszeit den kleineren der beiden Typen »(sizeof(T1) < sizeof(T2))« zurückgibt, ermittelt der zweite Ausdruck den Typ, in den sich die Typen »T1« und »T2« konvertieren lassen. Recht erhellend ist die Programmausgabe (Abbildung 5).
Besonders hässlich fallen die Type Traits aus, wenn der Entwickler an Stelle der automatischen Typ-Ableitung mit »auto« (Zeile 37) den expliziten Datentyp angibt (Zeile 36). Dann muss er die komplette Entscheidungslogik zum Ermitteln des Rückgabetyps wiederholen.
Was die Ausgabe des Programms auch schön zeigt: Im Fall des »gcdConditional()«-Algorithmus gibt sie den kleineren (Zeile 41), im Fall des »gcdCommon«-Algorithmus den größeren beider Datentypen (Zeile 42) zurück. Details zu beiden Funktionen warten einmal mehr in der Online-Dokumentation [6]. Dies betrifft auch die Funktion »std::enable_if()« im nächsten Abschnitt.
Wo ist die Funktion?
Konzeptionell besitzen alle bisher vorgestellten Variationen mit »std::is_integral<T1>::value« einen Schönheitsfehler: Ob ein Datentyp integral ist, testen sie erst im Körper der Funktions-Templates. Das ist eindeutig zu spät, dieser Test sollte bereits auf dem Interface der Funktion stattfinden.
Genau dies ermöglicht »std::enable_if()«. Die Funktion wendet einen Trick an, der in der C++-Community als Sfinae bekannt ist, einer Abkürzung für “Substitution Failure Is Not An Error” [11]. Das bedeutet: Wenn der Code ein Template wie »gcd(100, 100L)« instanziert, dann interpretiert der Compiler eine fehlerhafte Substituierung nicht als Fehler, sondern entfernt sie aus dem Pool möglicher Instanzierungen.
Listing 5
Sfinae mit std::enable_if() in der Anwendung
01 #include <iostream>
02 #include <type_traits>
03
04 template<typename T1, typename T2,
05 typename std::enable_if<std::is_integral<T1>::value, T1>::type = 0,
06 typename std::enable_if<std::is_integral<T2>::value, T2>::type = 0,
07 typename R = typename std::conditional <(sizeof(T1) < sizeof(T2)), T1, T2>::type>
08 R gcd(T1 a, T2 b){
09 if( b == 0 ){ return a; }
10 else{
11 return gcd(b, a % b);
12 }
13 }
14
15 int main(){
16
17 std::cout << "gcd(100, 10LL) = " << gcd(100, 10LL) << std::endl;
18 std::cout << "gcd(3.5, 4.0) = " << gcd(3.5, 4.0) << std::endl;
19
20 }
Häufig kennt der Compiler noch weitere Optionen, um das Template erfolgreich zu instanzieren. Hier kommt die Stärke der teilweisen oder vollständigen Template-Spezialisierung ins Spiel. Die Funktion »std::enable_if()« verpackt diese sehr hilfreiche Technik und wendet sie in Listing 5 an.
Die Fehlermeldung in Abbildung 6 bringt es ans Licht: “no matching function for call to ‘gcd(double, double)'”. Schlägt also das Instanzieren für »double« fehl, entfernt der Compiler diese konkrete Substitution aus der Menge aller möglichen Instanzierungen. In diesem Fall kann er aber auf keine weitere Option zurückgreifen. Die Instanzierung scheitert, da
std::enable_if<std::is_integral<T1>:: value, T1>::type
in Zeile 5 fehlschlägt. Dies gilt natürlich auch für die Zeile 6. Beide Zeilen sind nur Hilfsstrukturen, um Sfinae anwenden zu können.
Wie geht’s weiter?
Auf Tipp 8 folgt wenig überraschend Tipp 9, der da lautet: “Kenne deine Bibliotheken!” Diese Weisheit trifft insbesondere auf modernes C++ zu, weil jeder neue C++-Standard auch einige neue Bibliotheken im Gepäck hat.
Infos
- Eclipse: https://www.eclipse.org
- Visual Studio: https://visualstudio.com
- Thread Sanitzer: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
- Cpp Mem: http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/
- Smart Pointer: http://en.cppreference.com/w/cpp/memory
- Type Traits: http://en.cppreference.com/w/cpp/header/type_traits
- Das »auto«-Feature: http://en.cppreference.com/w/cpp/language/auto
- »static_assert«: http://en.cppreference.com/w/cpp/language/static_assert
- Euklidischer Algorithmus: https://de.wikipedia.org/wiki/Euklidischer_Algorithmus
- »std::is_integral«: http://en.cppreference.com/w/cpp/types/is_integral
- Sfinae: http://en.cppreference.com/w/cpp/language/sfinae












