Damit sein Code nicht als Bananensoftware beim Kunden reift, setzt der umsichtige C++-Lieferant auf die Funktion static_assert() und die Type-Traits-Bibliothek. Das dynamische Duo stellt Bedingungen an den Quellcode, die der Compiler zur Übersetzungszeit verifiziert.
Vorsicht ist die Mutter der Porzellankiste: Kontrolliert ein C++-Entwickler seinen Code bereits, noch während der Compiler ihn übersetzt, landen Fehler gar nicht erst in der Porzellankiste, also beim Kunden. Die Funktion »static_assert()« und die Type-Traits-Bibliothek helfen ihm dieses Ziel zu erreichen.
Kontrolle ist besser
Mit »static_assert()« lassen sich Bedingungen an den Quellcode stellen, die der Compiler beim Übersetzen testet. Damit schließt die Funktion ein Loch, das die Präprozessordirektive »#error« und das Makro »assert« hinterlassen. Denn »static_assert()« greift nach dem Präprozessorlauf ins Getriebe, aber noch bevor das System das Programm ausführt.
Die Syntax ist denkbar einfach: »static_assert(Ausdruck,Text)« . Während der Text, den die Software im Fehlerfall ausgibt, optional ist, muss C++ den Ausdruck zur Übersetzungszeit auswerten können, was keinen Einfluss auf die Laufzeit des Programms hat. Das unspektakuläre Listing 1 zeigt die Funktion in Aktion. Der Code lässt sich übersetzen, sofern ein »void*« -Zeiger auf der Plattform mindestens 8 Byte groß ist. Dem entgegen steht Listing 2, das nur eine Zeigergröße von 4 Bytes fordert.
Listing 2
static_assert() in verschiedenen Bereichen
01 static_assert(sizeof(void*) == 4, "32-bit addressing required");
02
03 namespace namespaceScope{
04 static_assert(sizeof(void*) == 4, "32-bit addressing required");
05 }
06
07 void functionScope(){
08 static_assert(sizeof(void*) == 4, "32-bit addressing required");
09 }
10
11 class ClassScope{
12 static_assert(sizeof(void*) == 4, "32-bit addressing required");
13 };
14
15 int main(){
16 {
17 static_assert(sizeof(void*) == 4, "32-bit addressing required");
18 }
19 }
Listing 1
Eine einfache Zusicherung
01 int main(){
02 static_assert(sizeof(void*) >= 8, "64-bit addressing is required for this program");
03 }
Wie Abbildung 1 zeigt, trifft diese zweite Forderung auf die Testmaschine nicht zu, die nach einer 64-Bit-Adressierung verlangt. Daneben zeigt die Abbildung vor allem zwei Dinge: Erstens dass »static_assert()« jeden Aufruf evaluiert, und zwar unabhängig davon, ob der Code tatsächlich zum Einsatz kommt. Dies gilt für den Aufruf im globalen Bereich (Zeile 3), im Funktionsbereich (Zeile 8), im Klassenbereich (Zeile 12) und im Blockbereich (Zeile 17). Zweitens zeigt es, dass die Fehlermeldung den Text des »static_assert()« -Aufrufs direkt verwendet: »error: static assertion 12: 32-bit adressing required« .
Seine volle Wirkung entfaltet »static_assert()« aber erst, wenn der Entwickler den Aufruf mit der Type-Traits-Bibliothek kombiniert.
Ein perfektes Paar
Da der Compiler die Funktionen der Type-Traits-Bibliothek beim Übersetzen ausführt, erweist diese sich als idealer Kandidat für die »static_assert()« -Funktion. Die braucht ja Ausdrücke, die der Compiler zur Übersetzungszeit evaluiert. Die Type-Traits-Bibliothek hat dabei einiges auf dem Kasten, allem voran kann sie Typen analysieren, vergleichen und sogar transformieren.
Vor der Kür aber kommt die Pflicht. Ziel des Artikels ist es, generische, typsichere Funktionen zu implementieren, die den größten gemeinsamen Teiler (GGT, [1]) zweier ganzer Zahlen berechnen. Die Funktionen sollen auf dem Algorithmus von Euklid ([2], Abbildung 2) basieren.
Erster Versuch
Der Algorithmus ist schnell als Funktions-Template in den Zeilen 3 bis 9 von Listing 3 implementiert. Er ist über seinen Typ »T« parametrisiert und gibt als Ergebnis den größten gemeinsamen Teiler seiner Argumente »a« und »b« zurück. In den Zeilen 15, 16 und 17 kommt schließlich der GGT-Algorithmus zum Zuge, das vielversprechende Ergebnis liefert Abbildung 4.
Listing 3
Der generische GGT-Algorithmus (Versuch 1)
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
19 /*
20
21 std::cout << gcd(3.5,4.0)<< std::endl;
22 std::cout << gcd("100","10") << std::endl;
23
24 std::cout << gcd(100,10L) << gcd(100,10L) << std::endl;
25
26 */
27
28 std::cout << std::endl;
29
30 }

Abbildung 4: Der generische GGT-Algorithmus liefert erste Ergebnisse (Versuch 1).
Allerdings leidet das Funktions-Template an zwei gravierenden Schwächen. Ruft der Nutzer GGT mit einer Fließkommazahl (Zeile 21) oder einem C-String (Zeile 22) auf, quittiert der Compiler dies mit einer Fehlermeldung. Der Entwickler hat für diese Typen nämlich keinen Modulo-Operator (»%« ) definiert.
Noch subtiler ist die zweite Schwäche, die in der Funktion »gcd()« steckt: Das Funktions-Template besitzt genau einen Template-Parameter. Das bedeutet insbesondere, dass beide Funktionsargumente »a« und »b« vom gleichen Typ sein müssen. Dies trifft aber auf den Ausdruck »gcd(100,10L)« in Zeile 24 nicht zu, da das erste Argument vom Typ »int« , das zweite hingegen vom Typ »long int« ist. Das weiß auch der Compiler. Er wendet keine Typkonvertierungen an, wenn er die Template-Parameter aus den Funktionsargumenten bestimmt. Leider drückt er sich dabei nicht so deutlich aus (Abbildung 3). Beide Schwächen bügelt die Type-Traits-Bibliothek aus.
Zweiter Versuch
Im ersten Fall ruft der Compiler den GGT-Algorithmus mit dem falschen Argument auf. Hier hilft ein »static_assert()« -Aufruf, der die Forderung nach seinen Argumenten explizit auf den Punkt bringt, es müssen Ganzzahlen sein: »std::is_integral<T>::value« . Listing 4 zeigt das leicht modifizierte Programm, das nun auch die Headerdatei »type_traits« benötigt. Netter Nebeneffekt: Die Fehlermeldung in Abbildung 5 ist nun bereits deutlich einfacher zu lesen.
Listing 4
Der generische GGT-Algorithmus (Versuch 2)
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 std::cout << gcd("100","10") << std::endl;
19
20 std::cout << std::endl;
21
22 }
Die finale Version
Die Anforderungen an den GGT-Algorithmus steigen jedoch weiter. So soll er nun über zwei Typen parametrisiert werden und den kleineren der beiden als Rückgabetyp zurückgeben. Relativ einfach kann der Entwickler die erste Anforderung umsetzen. Listing 5 zeigt den über zwei Typen parametrisierten Algorithmus. Die Idee von Listing 3 lässt sich direkt auf die zwei Template-Parameter in Listing 5 anwenden. Für jeden von ihnen prüft der Compiler im Funktionskörper einzeln (Zeilen 3 und 4), ob er eine Ganzzahl darstellt.
Listing 5
GGT über zwei Typen parametrisiert
01 template<typename T1, typename T2>
02 ??? gcd(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 gcd(b, a % b);
08 }
09 }
Offen bleibt jedoch die Frage, von welchem Typ der Rückgabewert (»???« ) in Zeile 2 ist. Hier greift wieder die Type-Traits-Bibliothek ein. Sie klärt, unterstützt vom Funktions-Template »std::conditional()« , ob es sich beim Rückgabetyp um Typ »T1« oder »T2« handelt. Der Ausdruck »std::conditional <(sizeof(T1) < sizeof(T2)), T1, T2>::type« in Zeile 5 von Listing 6 liefert den kleineren der beiden Typen zurück.
Listing 6
Der generische GGT-Algorithmus (finale Version)
01 #include <iostream>
02 #include <type_traits>
03 #include <typeinfo>
04
05 template<typename T1, typename T2, typename R = typename std::conditional <(sizeof(T1) < sizeof(T2)), T1, T2>::type >
06 R gcd(T1 a, T2 b){
07 static_assert(std::is_integral<T1>::value, "T1 should be an integral type!");
08 static_assert(std::is_integral<T2>::value, "T2 should be an integral type!");
09 if( b == 0 ){ return a; }
10 else{
11 return gcd(b, a % b);
12 }
13 }
14
15 int main(){
16
17 std::cout << std::endl;
18
19 std::cout << "gcd(100,10)= " << gcd(100,10) << std::endl;
20 std::cout << "gcd(100,33)= " << gcd(100,33) << std::endl;
21 std::cout << "gcd(100,0)= " << gcd(100,0) << std::endl;
22
23 std::cout << std::endl;
24
25 std::cout << "gcd(100,10LL)= " << gcd(100,10LL) << std::endl;
26
27 std::conditional <(sizeof(100) < sizeof(10LL)), long, long long>::type uglyRes= gcd(100,10LL);
28 auto res= gcd(100,10LL);
29 auto res2= gcd(100LL,10L);
30
31 std::cout << "typeid(gcd(100,10LL)).name(): " << typeid(res).name() << std::endl;
32 std::cout << "typeid(gcd(100LL,10L)).name(): " << typeid(res2).name() << std::endl;
33
34 std::cout << std::endl;
35
36 }
Für ihre Operation benötigt die Funktion insgesamt drei Argumente: einen Ausdruck sowie zwei Typen. Als Ausdruck dient »(sizeof(T1) < sizeof(T2))« . den der Compiler zur Übersetzungszeit evaluiert. Ist das Ergebnis des Ausdrucks wahr (»true« ), gibt »std::conditional()« den ersten Typ »T1« , sonst den zweiten Typ »T2« zurück.
Das Funktions-Template »std::conditional()« tritt hier als das Pendant zum bekannten ternären Operator »(sizeof(a) < sizeof(b)) ? a : b« auf. Zwei entscheidende Unterschiede gibt es jedoch: Einerseits arbeitet »std::condition()« mit Typen, während der ternäre Operator mit Werten operiert. Andererseits führt C++ »std::condition()« zur Übersetzungszeit aus, den ternären Operator hingegen zur Laufzeit des Programms.
Mit diesem hilfreichen Werkzeug in der Hand, lässt sich der kleinere der beiden Typen bestimmen. Dazu benötigt das Funktions-Template nur einen dritten Template-Parameter »R« , der den Rückgabetyp repräsentiert. Er erhält als Defaultwert den durch »std::conditional()« ermittelten Typ und wird in Zeile 6 von Listing 6 eingeführt.
Das Listing demonstriert zugleich, wie die Features in dem modernen C++ hübsch zusammenarbeiten, um einen typsicheren, aber auch generischen GGT-Algorithmus umzusetzen. Zugegeben, die Syntax des Funktions-Template ist nicht ganz einfach zu verdauen. So bestimmt zum Beispiel »typename std::conditional <(sizeof(T1) < sizeof(T2)), T1, T2>::type« (Zeile 5) den Standardrückgabetyp des Template.
Das Wort »typename« soll an dieser Stelle den Compiler davon überzeugen, dass der Gesamtausdruck einen Typ bezeichnet. Dieser hängt von den Typ-Parametern »T1« und »T2« ab. Mit ein bisschen Phantasie könnte der Ausdruck zum Beispiel auch eine statische Variable einer Klasse »conditional« sein.
Richtig interessant wird Listing 6 in Zeile 27. Um das Ergebnis der Berechnung in der Variablen »uglyRes« zu speichern, ist derselbe längliche und hässliche Ausdruck notwendig, der den Rückgabetyp des Template erzeugt. Er ist nicht nur unschön, sondern auch sehr fehleranfällig und redundant. So muss der C++-Entwickler die Typen des Funktions-Template »gcd(100,10LL)« sowohl in dem Ausdruck »sizeof(100) < sizeof(10LL)« als auch in den potenziellen Rückgabetypen »long, long long« verwenden.
Den Rückgabetyp ermittelt »auto« ohne Zutun. Die Zeilen 28 und 29 reduzieren den Schreibaufwand auf das Nötigste, denn eine als »auto« deklarierte Variable erhält vom Compiler automatisch den richtigen Typ.
Nun gilt es noch zu prüfen, ob der ermittelte Rückgabetyp der kleinere der beiden Eingabetypen ist. Diese Aufgabe erledigt der »typeid« -Operator, der die Headerdatei »typeinfo« benötigt. Der Operator wird zur Übersetzungszeit aktiv, nimmt Typen oder Ausdrücke an und gibt Typinformationen in Form eines Objekts zurück. Auf diesem »type_info« -Objekt lässt sich wie im Beispiel die Methode »name« aufrufen, die eine String-Repräsentation des Typs anfordert. Zwar ist in C++ nicht definiert, wie die String-Repräsentation eines Typs auszusehen hat, die Sprache gibt aber in den meisten Fällen eine ausreichend informative Antwort.
Genau dies demonstriert auch Abbildung 6. Der kleinere Typ des Ausdrucks »gcd(100,10LL)« besitzt die String-Repräsentation »i« , der des Ausdrucks »gcd(100LL,10L)« verwendet »l« . Im ersten Fall ermittelt der Compiler einen »int« -, im zweiten einen »long int« -Typ.
Wie geht es weiter?
Die Type-Traits-Bibliothek bietet ihre reichhaltige Funktionalität zur Übersetzungszeit an. Sie lässt sich einerseits verwenden, um den Code in Kombination mit »static_assert()« typsicher zu machen. Sie kann andererseits aber auch den resultierenden Code optimieren. Das zeigt »std::conditional()« im Falle des GGT-Algorithmus, der den kleineren der beiden Eingabetypen als Rückgabetyp zurückgibt.
Der C++11-Fan ahnt es womöglich bereits: Das ist bei Weitem noch nicht das komplette Repertoire der Type-Traits-Bibliothek. Grund genug also, im nächsten Artikel systematischer auf ihre Fähigkeiten einzugehen.
Infos
- Größter gemeinsamer Teiler: http://de.wikipedia.org/wiki/Gr%C3%B6%C3%9Fter_gemeinsamer_Teiler
- Euklid: http://de.wikipedia.org/wiki/Euklid











