constexpr Funktionen

Nach der Template-Metaprogrammierung und der Type-Traits-Bibliothek geht es heute vor allem um constexpr-Funktionen.

Lesezeit: 8 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 74 Beiträge
Von
  • Rainer Grimm

Nach der Template-Metaprogrammierung und der Type-Traits-Bibliothek geht es heute vor allem um constexpr-Funktionen.

Ich habe in den letzten Jahren bereits ein paar Beiträge über constexpr geschrieben. Hier ist meine Motivation: Erstens werde ich interessante Ähnlichkeiten zwischen constexpr Funktionen und Templaten aufzeigen. Zweitens möchte ich über die Verbesserungen von constexpr in C++20 schreiben. Und schließlich gehe ich auch auf consteval in C++20 ein. Wenn eine Theorie in meinen Beiträgen nicht ausführlich genug ist, werde ich auf frühere Beiträge verweisen. Beginnen wir mit einer kurzen Zusammenfassung, bevor ich auf die neuen Themen eingehe.

constexpr ermöglicht es, zur Compilezeit mit der typischen C++-Syntax zu programmieren. Konstante Ausdrücke mit constexpr können drei Formen haben.

  • sind implizit const.
  • müssen durch einen konstanten Ausdruck initialisiert werden.
constexpr double pi = 3,14;

constexpr Funktionen in C++14 sind recht komfortabel. Sie können

  • andere constexpr Funktionen aufrufen.
  • können Variablen haben, die durch einen konstanten Ausdruck initialisiert werden müssen.
  • können bedingte Ausdrücke oder Schleifen enthalten.
  • sind implizit inline.
  • können keine static oder thread_local Daten besitzen.
  • müssen einen Konstruktor besitzen, der ein konstanter Ausdruck ist.
  • können keine virtuellen Funktionen besitzen.
  • können keine virtuelle Basisklasse besitzen.

Die Regeln für constexpr Funktionen oder Memberfunktionen sind simpel. Der Einfachheit wegen, nenne ich beide Funktionen.

constexpr Funktionen müssen alle ihr Abhängigkeit zur Compliezeit auflösen können. Eine constexpr-Funktion zu sein, bedeutet nicht, dass die Funktion zur Compilezeit ausgeführt wird. Es bedeutet, dass die Funktion das Potenzial hat, zur Compilezeit ausgeführt zu werden. Eine constexpr-Funktion kann auch zur Runtime ausgeführt werden. Es ist oft eine Frage des Compilers und der Optimierungsstufe, ob eine constexpr Funktion zur Compilezeit oder zur Runtime ausgeführt wird. Es gibt zwei Kontexte, in denen eine constexpr-Funktion func zur Compilezeit ausgeführt werden muss.

  • Die constexpr Funktion wird in einem Kontext ausgeführt, der zur Compilezeit ausgewertet wird. Das kann ein static_assert-Ausdruck wie bei der type-traits-Bibliothek oder die Initialisierung eines C-Arrays sein.
  • Der Wert einer constexpr-Funktion wird mit constexpr angefordert: constexpr auto res = func(5);

Hier ist ein kleines Beispiel zur 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 << '\n';

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 << '\n';
std::cout << "gcd(a,b): " << j << '\n';

std::cout << '\n';

}

(1) berechnet das Ergebnis i zur Compilezeit und (2) j zur Runtime. Der Compiler würde sich beschweren, wenn ich j als constexpr deklariere: constexpr int j = gcd(a, b). Das Problem ist in diesem Fall, dass die Integer a und b keine konstanten Ausdrücke sind.

Die Ausgabe des Programms sollte nicht überraschen.

Die Überraschung kann jetzt beginnen. Die Magie zeige ich mit dem Compiler Explorer.

(1) im Programm constexpr14.cpp läuft auf die Konstante 11 im folgenden Ausdruck hinaus: mov DWORD PTR[rbp-4], 11 (Zeile 33 im Screenshot). Im Gegensatz dazu ist Zeile (2) ein Funktionsaufruf: call gcd(int, int) (Zeile 41 im Screenshot).

Nach dieser Zusammenfassung möchte ich auf die Gemeinsamkeiten von constexpr-Funktionen und Template-Metaprogrammierung eingehen.

constexpr-Funktionen haben viel mit der Template-Metaprogrammierung gemeinsam. Wer mit der Template-Metaprogrammierung nicht vertraut ist, sollte meine drei vorangegangenen Beiträge einen Eindruck vermitteln.

Hier ist das große Bild, das constexpr-Funktionen mit Template-Metaprogrammierung vergleicht:

Ich möchte meiner Tabelle noch ein paar Anmerkungen hinzufügen.

  • Ein Template-Metaprogramm wird zur Compilezeit ausgeführt, aber eine constexpr-Funktion kann zur Compilezeit oder zur Runtime ausgeführt werden.
  • Argumente eines Template-Metaprogramms können Typen, Nicht-Typen wie int oder auch Templates sein.
  • Zur Compilezeit gibt es keinen Zustand und daher auch keine Veränderung. Das bedeutet, dass die Template-Metaprogrammierung ein rein funktionaler Programmierstil ist. Hier sind die Merkmale aus der Perspektive des funktionalen Stils:
    • Bei der Template-Metaprogrammierung wird ein Wert nicht verändert, sondern jedes Mal ein neuer Wert zurückgegeben.
    • Die Steuerung einer for-Schleife durch das Erhöhen einer Variablen wie i ist zur Compilezeit nicht möglich: for (int i; i <= 10; ++i). Die Template-Metaprogrammierung ersetzt daher Schleifen durch Rekursion.
    • Bei der Template-Metaprogrammierung wird die bedingte Ausführung durch eine Template-Spezialisierung ersetzt.

Zugegeben, dieser Vergleich war recht knapp. Ein bildlicher Vergleich einer Metafunktion (siehe Template-Metaprogrammierung - Wie es funktioniert) und einer constexpr-Funktion sollte die offenen Fragen beantworten. Beide Funktionen berechnen die Fakultät einer Zahl.

  • Die Funktionsargumente einer constexpr-Funktion entsprechen den Template-Argumenten einer Metafunktion.
  • Eine constexpr-Funktion kann Variablen besitzen und diese verändern. Eine Metafunktion erzeugt einen neuen Wert.
  • Eine Metafunktion verwendet Rekursion, um eine Schleife zu simulieren.
  • Anstelle einer Endbedingung verwendet eine Metafunktion eine vollständige Spezialisierung eines Templates, um eine Schleife zu beenden. Außerdem verwendet eine Metafunktion eine teilweise oder vollständige Spezialisierung, um eine bedingte Ausführung wie if-Anweisungen durchzuführen.
  • Anstelle eines aktualisierten Wertes res erzeugt die Metafunktion in jeder Iteration einen neuen Wert.
  • Eine Metafunktion hat keine Rückgabeanweisung. Sie verwendet den Wert als Rückgabewert.

constexpr-Funktionen und Templates haben aber noch mehr gemeinsam.

Details zur Template-Instantiierung finden sich in meinen vorherigen Beitrag "Template-Instanziierung". Ich möchte hier nur die wichtigsten Fakten hervorheben.

Eine Template wie isSmaller wird zweimal syntaktisch geprüft:

template<typename T>
bool isSmaller(T fir, T sec){
return fir < sec;
}

isSmaller(5, 10); // (1)

std::unordered_set<int> set1;
std::unordered_set<int> set2;
  • Zuerst wird die Syntax der Template-Definition geprüft. Diese Prüfung ist nicht durch den C++-Standard gefordert, aber erlaubt und wird in der Regel von Compilern durchgeführt.
  • Zweitens leitet der Compiler die Template-Argumente aus den Funktionsargumenten ab. Er erstellt dabei für jedes Template-Aargument eine konkrete Funktion und überprüft deren Syntax. Dieser Instanziierungsprozess schlägt im Fall von std::unordered_set<int> (2) fehl, weil der Datentyp den <-Operator nicht unterstützt.

constexpr-Funktionen werden ebenfalls zweimal auf ihre Syntax geprüft.

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


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

int a= 11;
int b= 121;
constexpr int j= gcd(a, b); // (2)
  • Zunächst prüft der Compiler, ob die Funktion gcd zur Compilezeit ausgeführt werden kann. Das bedeutet im Wesentlichen, dass alle Abhängigkeiten einer constexpr-Funktion, wie zum Beispiel Funktion, die sie aufruft, constexpr sein müssen.
  • Der Compiler muss bei jedem Aufruf von gcd darüber hinaus prüfen, ob die Argumente konstante Ausdrücke sind. Das hat zur Folge, dass der erste Aufruf (1) gültig ist, der zweite (2) jedoch nicht.

Letztlich sind sich Templates und constexpr-Funktionen auch in Bezug auf die Sichtbarkeit ihrer Definition sehr ähnlich.

Zum Instanziieren eines Template muss dessen Definition sichtbar sein. Das Gleiche gilt für constexpr-Funktionen.

Im nächsten Beitrag schreibe ich über constexpr-Funktionen in C++20 und das neue C++20 Schlüsselwort consteval. ()