constexpr - Den Compiler rechnen lassen
Video
Code
#include <iostream>
using namespace std;
constexpr bool DEBUG_ENABLED = false;
enum class LogLevel
{
TRACE,
DEBUG,
INFO,
WARNING,
ERROR
};
constexpr char const *to_string(const LogLevel level)
{
switch (level)
{
case LogLevel::TRACE:
return "TRACE";
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
}
}
template <LogLevel level, typename TContent> inline void log(TContent const &content)
{
if constexpr (DEBUG_ENABLED || level > LogLevel::DEBUG)
{
constexpr auto level_string = to_string(level);
cout << "[" << level_string << "] " << content << endl;
}
}
int main()
{
log<LogLevel::TRACE>("Just a test");
log<LogLevel::INFO>(12345);
return 0;
}
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.