C++ Core Guidelines: Programmierung zur Compilezeit mit constexpr

Modernes C++  –  5 Kommentare

Diese Serie zur Programmierung zur Compilezeit begann mit der Template-Metaprogrammierung, gefolgt von der Type-Traits-Bibliothek und endet heute mit konstanten Ausdrücken (constexpr).

Jetzt sind wir endlich an der Spitze des Dreiecks angekommen. Dies ist aber mehr als nur ein Bild.

constexpr

constexpr erlaubt es explizit zur Compilezeit zu programmieren und dies in der vertrauten C++-Syntax. Der Fokus dieses Artikels liegt nicht darin, alle Details zu constexpr vorzustellen, sondern vielmehr darin, Template-Metaprogrammierung mit constexpr-Funktionen zu vergleichen. Des Vergleich willens, möchte ich aber erst einen kurzen Überblick zu constexpr geben. Falls dieser Vergleich zu mager ist, bieten meine früheren Artikel zu constexpr ausreichend Nahrung. Welche Vorteile besitzen konstante Ausdrücke?

Vorteile

Ein konstanter Ausdruck

  • kann zur Compilezeit ausgewertet werden.
  • erlaubt dem Compiler tiefen Einblick in den Code.
  • ist implizit thread-sicher.
  • kann im read-only Speicher (ROM-able) erzeugt werden.

Konstante Ausdrücke mit constexpr können drei Formen besitzen.

Drei Formen

Variablen

  • sind implizit konstant.
  • müssen durch einen konstanten Ausdruck initialisiert werden: constexpr double pi = 3.14;

Funktionen

constexpr Funktionen in C++14 sind sehr einfach einzusetzen. Sie sind implizit inline und können

  • andere constexpr-Funktionen aufrufen.
  • Variablen besitzen, die durch einen konstanten Ausdruck initialisiert werden müssen.
  • bedingte Anweisungen oder Schleifen besitzen.
  • keine static- oder thread_local-Daten besitzen.

Benutzerdefinierte Typen

  • müssen einen Konstruktor besitzen, der selbst ein konstanter Ausdruck ist.
  • können keine virtuelle Methoden besitzen.
  • können keine virtuellen Basisklassen besitzen.

Die Regeln für constexpr-Funktionen und -Methoden sind ziemlich ähnlich. Daher spreche ich nun nur noch von Funktionen.

constexpr-Funktionen dürfen nur von Funktionalität abhängen, die einen konstanten Ausdruck darstellt. Eine constexpr-Funktion bedeutet nicht, dass eine Funktion zur Compilezeit ausgeführt wird. Es bedeutet, dass eine Funktion das Potenzial besitzt, zur Compilezeit ausgeführt zu werden. Eine constexpr-Funktion kann auch zur Laufzeit ausgeführt werden. Es ist oft eine Frage des Compilers oder der Optimierung, ob eine Funktion zur Compile- oder Laufzeit ausgeführt wird. Es gibt aber zwei Gründe, warum constexpr-Funktionen zur Compilezeit ausgeführt werden müssen.

  1. Die constexpr-Funktion wird in einem Kontext eingesetzt, der zur Compilezeit ausgewertet wird. Dies kann ein static_assert-Ausdruck oder die Initialisierung eines C-Arrays sein.
  2. Der Wert eine constexpr-Funktion wird explizit zur Compilezeit angefordert: constexpr auto res = func(5);

Hier kommt ein kleines Beispiel zu der Theorie. Das Programm constexpr14.cpp berechnet den größten gemeinsamen Teiler zweier Zahlen.

// constexpr14.cpp

#include <iostream>

constexpr auto gcd(int a, int b){
while (b != 0){
auto t= b;
b= a % b;
a= t;
}
return a;
}

int main(){

std::cout << std::endl;

constexpr int i= gcd(11,121); // (1)

int a= 11;
int b= 121;
int j= gcd(a,b); // (2)

std::cout << "gcd(11,121): " << i << std::endl;
std::cout << "gcd(a,b): " << j << std::endl;

std::cout << std::endl;

}

Die Zeile (1) berechnet das Ergebnis i zur Compilezeit und die Zeile (2) zur Laufzeit. Der Compiler würde sich eindeutig beschweren, wenn ich j als constexpr erklären würde: constexpr int j = gcd(a, b). Der Grund ist, dass weder a noch b konstante Ausdrücke sind.

Die Ausgabe des Programms sollte keine Überraschung bergen.

Vielleicht geht ja die Überraschung jetzt los. Lass mich die Magie mit dem Compiler Explorer vorstellen:

Die Zeile (1) des Programms constexpr14.cpp reduziert sich auf die Konstante 11 in dem folgenden Ausdruck: mov DWORD PTR[rbp-4], 11 (Zeile 33 im Screenshot). Im Gegensatz dazu, wird die Zeile (2) zu einem Funktionsaufruf: call gcd(int, int) (Zeile 41 in dem Screenshot).

Jetz kann ich endlich mein Hauptanliegen vorstellen.

Template-Metaprogrammierung versus constexpr-Funktionen

Zuerst einmal das große Bild:

Die Tabelle verlangt ein paar Erläuterungen.

  • Ein Template-Metaprogramm wird zur Compilezeit ausgeführt, aber eine constexpr-Funktion (siehe constexpr14.cpp) kann sowohl zur Compile- als auch zur Laufzeit ausgeführt werden.
  • Argumente eines Templates (Template-Metaprogrammierung) können Typen und Werte sein. Um genauer zu sein, ein Template kann Datentypen (std::vector<int>), Werte (std:.array<int, 5>) und selbst Templates (std::stack<int, std::vector<int>>) annehmen. constexpr-Funktionen sind vor allem Funktionen, die das Potenzial besitzen, zur Compilezeit ausgeführt zu werden. Daher nehmen sie nur Werte an.
  • Es gibt keinen Zustand zur Compilezeit und damit auch keine Veränderung. Das heißt, Template-Metaprogrammierung ist reine funktionale Programmierung. Was? Falls du wissen willst, für was reine funktionale Programmierung steht, gibt der Funktionale Programmierung: Die Definition die erste und die Artikel Funktional die genaue Antwort. Hier sind wichtigen Punkte:
    • Statt einen Wert zu verändern, wird immer ein neuer Wert in der Template-Metaprogrammierung zurückgegeben.
    • Das Inkrementieren eines Wertes i in einer for-Schleife ist zur Compilezeit nicht möglich: for (int i; i <= 10; ++i). Daher ersetzt die Template-Metaprogrammierung Schleifen mit Rekursionen.
    • Dank Template-Spezialisierung ist eine bedingte Ausführung möglich.

Zugegeben, dieser Vergleich ist sehr kurz und knapp. Eine bildliche Gegenüberstellung von Metafunktionen und constexpr-Funktionen wird die offenen Fragen beantworten. Beide Funktionen berechnen die Fakultät einer Zahl.

Die Funktionsargumente der constexpr-Funktion entsprechen den Template-Argumenten der Metafunktion.

Eine constexpr-Funktion kann Variablen besitzen und sie verändern. Eine Metafunktion erzeugt immer einen neuen Wert.

Eine Metafunktion stellt Schleifen durch Rekursion dar.

Anstelle einer Endbedingung, verwendet eine Metafunktion eine vollständige Spezialisierung zur Beendigung einer Rekursion. Zusätzlich erlaubt teilweise oder vollständige Spezialisierung eines Templates bedingt Codeausführung entsprechend einer if-Anweisung.

Anstelle eines veränderten Wertes res, erzeugt eine Metafunktion immer einen neuen Wert.

Eine Metafunktion besitzt keine return-Anweisung. Stattdessen verwendet sie value als Rückgabewert.

Vorteile von constexpr-Funktionen

Neben den Vorteilen, dass constexpr-Funktionen komfortabler zu schreiben und zu pflegen sind und zur Laufzeit ausgeführt werden können, besitzen sie noch einen weiteren Vorteil. Der Codeschnipsel stellt ihn vor:

constexpr double average(double fir , double sec){
return (fir + sec) / 2;
}

int main(){
constexpr double res = average(2, 3);
}

constexpr-Funktionen können mit Fließkommazahlen umgehen. Template-Metaprogrammierung verlangt Ganzzahlen.

Wie geht's weiter?

Dieser Artikel beendet meinen Umweg zur Programmierung zur Compilezeit. Das nächste Mal schreibe ich über die verbleibenden Regeln zu Templates.