constexpr Funktionen
Nach der Template-Metaprogrammierung und der Type-Traits-Bibliothek geht es heute vor allem um constexpr-Funktionen.
- 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.
Ein kurzer RĂĽckblick
constexpr ermöglicht es, zur Compilezeit mit der typischen C++-Syntax zu programmieren. Konstante Ausdrücke mit constexpr können drei Formen haben.
Variablen
- sind implizit const.
- mĂĽssen durch einen konstanten Ausdruck initialisiert werden.
constexpr double pi = 3,14;
Funktionen
constexpr Funktionen in C++14 sind recht komfortabel. Sie können
- andere
constexprFunktionen 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_localDaten besitzen.
Benutzerdefinierte Typen
- 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
constexprFunktion wird in einem Kontext ausgefĂĽhrt, der zur Compilezeit ausgewertet wird. Das kann einstatic_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.
Template-Metaprogrammierung
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.
- Template-Metaprogrammierung - Wie alles begann
- Template-Metaprogrammierung - Wie es funktioniert
- Template Metaprogrammierung - Hybride Programmierung
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
intoder 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 wieiist 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
reserzeugt 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.
Instanziierung von Templates
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
gcdzur Compilezeit ausgeführt werden kann. Das bedeutet im Wesentlichen, dass alle Abhängigkeiten einerconstexpr-Funktion, wie zum Beispiel Funktion, die sie aufruft,constexprsein müssen. - Der Compiler muss bei jedem Aufruf von
gcddarĂĽ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.
Sichtbarkeit
Zum Instanziieren eines Template muss dessen Definition sichtbar sein. Das Gleiche gilt fĂĽr constexpr-Funktionen.
Wie geht's weiter?
Im nächsten Beitrag schreibe ich über constexpr-Funktionen in C++20 und das neue C++20 Schlüsselwort consteval.
()