Open Source im professionellen Einsatz
Linux-Magazin 06/2015

Modernes C++ in der Praxis – Folge 22

Konstante Magie

Indem er das Zauberwort "constexpr" anwendet, erreicht der Programmierer, dass C++11 seine Ausdrücke bereits beim Kompilieren auswertet. Folgt er dabei den vorgefertigen Beschwörungsritualen, generieren solche konstanten Ausdrücke Performancegewinne.

1306

Die durch »constexpr« erklärten Ausdrücke heißen konstante Ausdrücke. Es darf sich um Variablen, eigene Datentypen, aber auch Funktionen handeln. Ihr Vorteil liegt auf der Hand: Sie sind zur Laufzeit bereits vollständig evaluiert. Das reduziert die Ausführungszeit eines Programms, macht die Ausdrücke implizit Thread-sicher und speichert sie im gut verfügbaren, nicht flüchtigen Speicher.

Lage der Dinge

Die »constexpr« -Variablen und insbesondere -Funktionen zählen in C++11 eher zu den Neuankömmlingen. Als konstante Ausdrücke erklärte Variablen sind implizit konstant, das heißt, sie lassen sich nach ihrer Deklaration nicht mehr verändern.

Versieht der Entwickler etwa die Variable »double myDouble= 5.2« mit dem Schlüsselwort »constexpr« , verwandelt er sie in den konstanten Ausdruck »constexpr double myDouble= 5.2« . Dazu aber muss er die Variable mit einem konstanten Ausdruck initialisieren. Solche »constexpr« -Variablen sind implizit konstant. Listing 1 erklärt neben Variablen auch Funktionen als konstante Ausdrücke. Die Ausgabe des kleinen Programms stellt Abbildung 1 dar.

Listing 1

constexpr.cpp

01 #include <iostream>
02
03 constexpr int square(int x) { return x * x; }
04 constexpr int squareToSquare(int x){ return square(square(x));}
05
06 int main() {
07
08   std::cout << std::endl;
09
10   static_assert(square(10) == 100,"you calculated it wrong");
11   static_assert(squareToSquare(10) == 10000 ,"you calculated it wrong");
12   auto ten=10;
13   int square10= square(10);
14
15   std::cout<< "square(10)= " << square(10) << std::endl;
16   std::cout<< "squareToSquare(10)= " << squareToSquare(10) << std::endl;
17
18   constexpr int constExpr= square(10);
19   int arrayClassic[100];
20   int arrayNewWithConstExpression[constExpr];
21   int arrayNewWithConstExpressioFunction[square(10)];
22
23   std::cout << std::endl;
24
25 }
Abbildung 1: Einfach zauberhaft: Sowohl Variablen als auch Funktionen lassen sich in C++11 zu konstanten Ausdrücken erklären.

Zunächst zur Variablen »constExpr« in Zeile 18. Erklärt der Programmierer sie zum konstanten Ausdruck, muss er sie auch entsprechend initialisieren. Da »square(10)« als Funktion selbst ein konstanter Ausdruck ist, passt die Initialisierung syntaktisch. Wer »constexpr« -Variablen verwendet, sollte zwei Dinge im Kopf behalten. Versucht er erstens, eine als konstanter Ausdruck erklärte Variable durch einen nicht konstanten Ausdruck zu initialisieren, quittiert der Compiler dies mit einer eindeutigen Fehlermeldung. Zweitens kann er aber konstante Ausdrücke auch dazu verwenden, nicht konstante Ausdrücke aus der Taufe zu heben. Dies ist in Zeile 13 der Fall: »int square10= square(10)« .

Als »constexpr« erklärte Funktionen zeigen sich hingegen schon deutlich anspruchsvoller. So sind die beiden Funktionen »square()« und »squareToSquare()« (Zeilen 3 und 4) konstante Ausdrücke. Das bedeutet explizit, dass C++11 sie zur Übersetzungszeit evaluiert. Schön zeigen dies die »static_assert« -Anweisungen in den Zeilen 10 und 11, die der Compiler verifiziert. Dass »square(10)« ein konstanter Ausdruck ist, lässt sich aber auch indirekt zeigen, denn die Größenangabe eines Array muss ein konstanter Ausdruck sein (Zeile 21).

Als konstante Ausdrücke erklärte Funktionen unterliegen in C++11 sehr vielen Einschränkungen. Sie müssen einen Wert zurückliefern, lassen sich nur mit konstanten Ausdrücken aufrufen oder müssen selbst konstante Ausdrücke initialisieren. Das ist aber noch nicht alles: Sie dürfen nur aus einer Rückgabe-Anweisung bestehen und sind implizit als »inline« -Funktionen markiert.

Das ändert sich mit C++14 deutlich. Die Liste an Einschränkungen bei C++11 weicht einer knapperen Negativliste, die nur noch wenige Einträge umfasst.

Datentypen im Eigenbau

In einem früheren Artikel [1] zeigte der Autor einen eigenen Datentyp, der das Rechnen mit Entfernungen unterstützt. Die Technik basiert auf dem neuen C++11-Feature der benutzerdefinierten Literale. Deren entscheidender Vorteil besteht darin, dass ein Entwickler die Built-in-Literale um eigene Einheiten erweitern kann.

Listing 2 stellt eine verkürzte, aber zugleich erweiterte Form des Programms vor. Sie berechnet anspruchsvolle arithmetische Ausdrücke, wobei der Compiler automatisch für die richtige Einheit sorgt. Im konkreten Beispiel liegen alle Ergebnisse in Metern vor.

Listing 2

constexprType.cpp (C++11)

01 #include <iostream>
02 #include <ostream>
03
04 namespace Distance{
05   class MyDistance{
06     public:
07       constexpr MyDistance(double i):m(i){}
08
09       friend constexpr MyDistance operator +(const MyDistance& a, const MyDistance& b){
10         return MyDistance(a.m + b.m);
11       }
12       friend constexpr MyDistance operator -(const MyDistance& a, const MyDistance& b){
13         return MyDistance(a.m - b.m);
14       }
15
16       friend constexpr MyDistance operator*(double m, const MyDistance& a){
17         return MyDistance(m*a.m);
18       }
19
20       friend std::ostream& operator<< (std::ostream &out, const MyDistance& myDist){
21         out << myDist.m << " m";
22         return out;
23       }
24     private:
25       double m;
26
27   };
28
29   namespace Unit{
30     MyDistance constexpr operator "" _km(long double d){
31       return MyDistance(1000*d);
32     }
33     MyDistance constexpr operator "" _m(long double m){
34       return MyDistance(m);
35     }
36     MyDistance constexpr operator "" _dm(long double d){
37       return MyDistance(d/10);
38     }
39     MyDistance constexpr operator "" _cm(long double c){
40       return MyDistance(c/100);
41     }
42   }
43 }
44
45 using namespace Distance::Unit;
46
47 int main(){
48
49   std:: cout << std::endl;
50
51   std::cout << "1.0_km: " << 1.0_km << std::endl;
52   std::cout << "1.0_m: " << 1.0_m << std::endl;
53   std::cout << "1.0_dm: " << 1.0_dm << std::endl;
54   std::cout << "1.0_cm: " << 1.0_cm << std::endl;
55   std::cout << "1.5_km + 105.1_m: " << .5_km + 105.1_m << std::endl;
56
57   std::cout << std::endl;
58
59   constexpr Distance::MyDistance work= 63.0_km;
60   constexpr Distance::MyDistance workPerDay= 2 * work;
61   constexpr Distance::MyDistance abbrevationToWork= 5400.0_m;
62   constexpr Distance::MyDistance workout= 2 * 1600.0_m;
63   constexpr Distance::MyDistance shopping= 2 * 1200.0_m;
64
65   constexpr Distance::MyDistance myDistancePerWeek= 4 * workPerDay - 3 * abbrevationToWork + workout + shopping;
66
67   std::cout << "4 * workPerDay - 3 * abbrevationToWork + workout + shopping: " << myDistancePerWeek;
68
69   std::cout << "\n\n";
70
71 }

Abbildung 2 enthüllt schematisch die Magie, die der Compiler beim Übersetzen auf Listing 2 anwendet. Sie zeigt sich beispielsweise in der Addition der beiden Ausdrücke »1.5_km + 105.1_m« (Zeile 55). Im ersten Schritt bildet der Compiler die benutzerdefinierten Literale »1.5_km« und »105.1_m« auf die literalen Operatoren »operator"" _km(1.5)« (Zeile 30) sowie »operator"" _m(105.1)« (Zeile 33) ab.

Abbildung 2: Die Magie des Compilers enthüllt Listing 2.

Im Funktionskörper der Literal Operators setzt er beide anschließend über »MyDistance(1500.0)« (Zeile 31) beziehungsweise »MyDistance(105.1)« (Zeile 34) auf die Spur. Zum Abschluss kommt der »+« -Operator in Zeile 9 zum Tragen. Mit Hilfe des überladenen Ausgabe-Operators in Zeile 20 stellt er das Ergebnis der Addition dar (Abbildung 3). Beeindruckend ist auch die Variable »myDistancePerWeek« in Zeile 67. Sie zeigt die Anzahl der Meter, die der Autor pro Woche mit dem Auto zurücklegt.

Abbildung 3: Auch in der Arithmetik mit eigenen Datentypen machen sich die konstanten Ausdrücke gut.

Manch einer mag sich nun fragen, ob der Artikel hier nur geschickt Listings aus älteren wiederkäut. Selbstverständlich nicht. Benutzerdefinierte Literale sind vielmehr ein ideales Einsatzgebiet für »constexpr« -Funktionen – und genau dies demonstriert der Code. In ihm machen die konstanten Ausdrücke alle Instanzen von »MyDistance« Thread-sicher und speichern sie als Konstanten im nicht flüchtigen Speicher.

Wer aber »MyDistance« zur Übersetzungszeit instanzieren möchte, sollte ein paar Regeln beachten. So muss »constexpr« in Zeile 7 ein Konstruktor sein und einen leeren Funktionskörper besitzen. Instanzen von »MyDistance« dürfen konsequenterweise zur Kompilierzeit nur Methoden aufrufen, die selbst konstante Ausdrücke sind.

Hier hilft genaueres Hinsehen: Die »main« -Funktion mal ausgenommen hat der Programmierer nicht gleich jede Variable und jede Funktion in Listing 2 als »constexpr« deklariert. Der Ausgabe-Operator in Zeile 20 ist kein konstanter Ausdruck, wird also zur Laufzeit und nicht beim Übersetzen evaluiert. Das hat zwei Gründe: Zum einen besteht er aus zwei Ausdrücken, zum anderen ruft er mit »std::cout« einen nicht konstanten Ausdruck auf.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 4 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • C++11

    C++11 kennt zwei neue Literale: Raw-String- und benutzerdefinierte Literale. Während Raw-String-Literale den Interpreter zum Beispiel davon abbringen, Backslashes in Zeichenketten zu interpretieren, schützen benutzerdefinierte Literale dereinst womöglich Nasa-Sonden vor dem Verglühen.

  • C++

    Mit der Move-Semantik und konstanten Ausdrücken besitzt modernes C++ eine kräftige Stellschraube, um die Performance einer C++-Anwendung in die richtige Richtung zu drehen.

  • Erfrischend neu

    An der Sprache C++ ändert sich selten etwas. Doch nun steht mit dem C++0x ein neuer Standard vor der Tür, von dem Neulinge wie Könner profitieren sollen. Mit Move-Semantik, Lambda-Funktionen und Aggregatzuweisungen bringt er viele Modernisierungen in die Programmiersprache.

comments powered by Disqus

Ausgabe 10/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.