constexpr - Den Compiler rechnen lassen
Video
Code
|
|
Erklärung
constexpr
sind eine Möglichkeit, ab C++ 11 (oder so richtig benutzbar ab C++ 14/17) Sachen vom Compiler zur Compilezeit übersetzen zu lassen. Bisher musste man für derartige Sachen auf Dinge wie Template Metaprogramming zurückgreifen. Das war und ist natürlich ein mächtiges Sprachfeature, mit dem sich interessante Probleme lösen lassen, aber für viele Sachen (und für viele Programmierer) werden derartige Programme schnell unübersichtlich. “Klassischer” C++-Code ist oft einfach besser lesbar, als der fast schon funktionale Stil eines rekursiven Templates.
Mit C++ 11 wurden erstmal constexpr
-Ausdrücke eingeführt. Das sind (damals noch sehr beschränkte) Funktionen, die der Compiler zur Compilezeit auswerten und deren Rückgabewert er wie eine Compile-Time-Constant verwenden kann. Das Stichwort “Compile Time” ist dabei wichtig: im Gegensatz zu Runtime Constants (die mit dem const
-Schlüsselwort, die zwar konstant sind, aber zur Laufzeit initialisiert werden) sind Compile-Time-Constants direkt im Programmcode hinterlegt und können überall da verwendet werden, wo man normalerweise auch ein Literal eines Wertes hinterlegen könnte. Mit C++ 14 wurden viele der Beschränkungen der ursprünglichen constexpr
aufgehoben. Die Funktionen dürfen nun aus mehreren Anweisungen bestehen und auch mehr als ein return
haben. Schleifen und if
-Bedingunen werden möglich, was die Verwendbarkeit des Features deutlich erhöht. Weiterhin zwingend bleibt die Verwendung von constexpr
als Input. Wenn der Compiler die constexpr
zur Compilezeit auswerten soll, dann müssen natürlich auch alle Eingabewerte zur Compilezeit vorliegen.
Das Beispiel führt constexpr
in der Methode to_string()
ab Zeile 16 vor. Die Funktion übersetzt zwischen den Werten des LogLevel
-enum
und ihrer menschenlesbaren Repräsentation als Zeichenkette. Durch die Verwendung des constexpr
-Schlüsselwortes wird der Compiler angewiesen, die Auswertung zur Compilezeit zumindest zu prüfen (erzwungen wird sie im Beispiel letztlich durch die Zuweisung an constexpr auto level_string
in Zeile 37). Wird die Auswertung zur Compilezeit erfolgreich durchgeführt, dann wird der komplette Funktionsaufruf durch den statischen Rückgabewert ersetzt. Im Video ist das erkennbar durch die direkte Einbettung der Zeichenkette "INFO"
in den Assemblercode. Einen Funktionskörper von to_string()
sucht man dort hingegen vergeblich. Er wird schlicht nicht mehr gebraucht.
Ein weiteres Feature im Bereich constexpr
ist die Einführung von if constexpr
mit C++ 17. Diese speziellen if
-Anweisungen ermöglichen eine bedingte Kompilierung ähnlich wie Templatespezialisierungen, allerdings flexibler und übersichtlicher in “normalen” Code eingebettet. if constexpr
können auf alles zurückgreifen, was seinerseits wieder eine Compile-Time-Constant ist und über diese Bedingungen bilden. Wird die entsprechende Bedingung true
, so übersetzt der Compiler den zugehörigen if
-Block. Ist die Bedingung false
, so unterbleibt das.
Der bedingt übersetzte Code muss ähnlich wie bei Template-Spezialisierungen nur syntaktisch korrekt sein. Ob er semantisch gerade Sinn ergibt (bspw. weil eine aufgerufene Methode am Zieltyp nicht vorhanden ist), ist irrelevant, solange der entsprechende Block nicht kompiliert werden soll. Man kann if constexpr
also auch einsetzen, um auf das Vorhandensein von bestimmten Eigenschaften an Typen zu prüfen und abhängig vom Ergebnis bestimmte Aufrufe durchzuführen oder eben nicht.
Im Beispiel entscheided die if constexpr
aus, ob eine Logausgabe überhaupt stattfinden soll. Bedingung dafür ist entweder, dass alle Ausgaben getätigt werden sollen (DEBUG_ENABLED
also true
ist) oder dass die auszugebende Nachricht eine “normale” Nachricht (im Gegensatz zu einer Debug-Nachricht) ist. Ist die entsprechende Prüfung erfolgreich, wir der im if
-Block enthaltene Code kompiliert und zur Laufzeit eben auch ausgeführt. Ist die Bedingung unwahr, wird der Code entsprechend nicht eingesetzt. Damit wird die gesamte Methode leer und vom Optimierer aus der Ausgabe entfernt.
Abhängig von DEBUG_ENABLED
sind also beide Aufrufe der Funktion (Zeilen 44 und 45) in der Ausgabe enthalten oder nur Zeile 45, und das deutlich lesbarer, als mit Template-Spezialisierungen.