zurück zum Artikel

Zwei neue Schlüsselwörter in C++20: consteval und constinit

Rainer Grimm

Zwei neue Schlüsselwörter in C++20: consteval und constinit. Das erste erzeugt eine Funktion, die zur Compilezeit ausgeführt wird, das zweite sichert zu, dass eine Variable zur Compilezeit initialisiert wird.

Mit C++20 erhalten wir zwei neue Schlüsselwörter: consteval und constinit. Das erste erzeugt eine Funktion, die zur Compilezeit ausgeführt wird, das zweite sichert zu, dass eine Variable zur Compilezeit initialisiert wird.

C++20: consteval und constinit

Beim Lesen meiner kurzen Einleitung zu consteval und constinit mag der Eindruck entstehen, dass beide Spezifizierer sehr ähnlich zu constexpr sind. Diese Beobachtung trifft zu. Bevor ich aber die Schlüsselwörter consteval, constinit, constexpr und das gute alte const genauer unter die Lupe nehmen, möchte ich consteval und constinit vorstellen.

consteval int sqr(int n) {
return n * n;
}

consteval erzeugt eine sogenannte immediate-Funktion. Jeder Aufruf einer solchen Funktion erzeugt eine Compilezeit-Konstante. Dies lässt sich auch einfacher ausdrücken. Eine consteval- (immediate-)Funktion wird zur Compilezeit ausgeführt.

consteval kann nicht auf Destruktoren oder Funktionen angewandt werden, die Speicher allokieren oder freigeben. In einer Deklaration lässt sich nur maximal eines der Schlüsselwörter consteval, constexpr oder constinit einsetzen. Eine immediate-Funktion (consteval) ist implizit inline und muss dieselben Anforderungen wie eine constexpr-Funktion erfüllen.

Die Anforderungen an eine constexpr-Funktion in C++14 und damit eine consteval-Funktion sind die folgenden: Eine constexpr-Funktion kann

constexpr-Funktionen können keine statischen oder thread_local-Daten verwenden. Auch ist ein try-Block oder eine goto-Anweisungen nicht möglich. Das folgende Programm constevalSqr.cpp stellt die consteval-Funktion sqr vor.

// constevalSqr.cpp

#include <iostream>

consteval int sqr(int n) {
return n * n;
}

int main() {

std::cout << "sqr(5): " << sqr(5) << std::endl; // (1)

const int a = 5; // (2)
std::cout << "sqr(a): " << sqr(a) << std::endl;

int b = 5; // (3)
// std::cout << "sqr(b): " << sqr(b) << std::endl; ERROR

}

5 ist ein konstanter Ausdruck und lässt sich damit als Argument der Funktion sqr (1) einsetzen. Dasselbe gilt die für Variable a (2). Eine Konstante wie a lässt sich in einem konstanten Ausdruck verwenden, wenn sie mit einem konstanten Ausdruck initialisiert wird. b (3) ist kein konstanter Ausdruck. Konsequenterweise ist der Aufruf sqr(5) nicht gültig.

Dank des neuen GCC11-Compilers und dem Compiler Explorer [1] kann ich das Programm ausführen.

C++20: consteval und constinit

constinit kann auf Variablen mit statischer Speicherdauer (static storage duration) oder Thread-Speicherdauer (thread storage duration) angewandt werden.

constinit sichert für diese Variablen (statische Speicherdauer und Thread-Speicherdauer) zu, dass sie zur Compilezeit initialisiert werden:

// constinitSqr.cpp

#include <iostream>

consteval int sqr(int n) {
return n * n;
}

constexpr auto res1 = sqr(5);
constinit auto res2 = sqr(5);

int main() {

std::cout << "sqr(5): " << res1 << std::endl;
std::cout << "sqr(5): " << res2 << std::endl;

constinit thread_local auto res3 = sqr(5);
std::cout << "sqr(5): " << res3 << std::endl;

}

res1 und res2 besitzen statische Speicherdauer. res3 besitzt Thread-Speicherdauer.

C++20: consteval und constinit

Nun ist es an der Zeit, auf die Unterschiede von const, constexpr, consteval und constinit genauer einzugehen. Diese Unterschiede werde ich anhand des Ausführens einer Funktion und des Initialisierens einer Variable vorstellen.

Das Programm consteval.cpp besitzt drei Versionen der square-Funktion:

// consteval.cpp

#include <iostream>

int sqrRunTime(int n) {
return n * n;
}

consteval int sqrCompileTime(int n) {
return n * n;
}

constexpr int sqrRunOrCompileTime(int n) {
return n * n;
}

int main() {

// constexpr int prod1 = sqrRunTime(100); ERROR (1)
constexpr int prod2 = sqrCompileTime(100);
constexpr int prod3 = sqrRunOrCompileTime(100);

int x = 100;

int prod4 = sqrRunTime(x);
// int prod5 = sqrCompileTime(x); ERROR (2)
int prod6 = sqrRunOrCompileTime(x);

}

Die Namen deuten es bereits an: Die gewöhnliche Funktion sqrRunTime wird zur Laufzeit ausgeführt; die consteval-Funktion sqrCompileTime zur Compilezeit; die constexpr Funktion sqrRunOrCompileTime zur Laufzeit oder Compilezeit. Konsequenterweise führt der Aufruf der Funktion sqrRunTime (1) zur Compilezeit zu einem Fehler. Ein Fehler ergibt sich, wenn ein nichtkonstaner Ausdruck als Argument der Funktion sqrCompileTime (2) verwendet wird.

Der Unterschied zwischen constexpr-Funktion sqrRunOrCompileTime und der consteval-Funktion sqrCompileTime ist, dass die erste nur dann zur Compilezeit ausgeführt werden muss, wenn das ihr Aufrufkontext erfordert:

static_assert(sqrRunOrCompileTime(100) == 100);                   // compile-time (1)
int arrayNewWithConstExpressioFunction[sqrRunOrCompileTime(100)]; // compile-time (1)
constexpr int prod = sqrRunOrCompileTime(100); // compile-time (1)

int a = 100;
int runTime = sqrRunOrCompileTime(a); // run-time (2)

int runTimeOrCompiletime = sqrRunOrCompileTime(100); // run-time or compile-time (3)

int allwaysCompileTime = sqrCompileTime(100); // compile-time (4)

Die ersten drei Zeilen (1) fordern die Ausführung zur Compilezeit. Zeile (2) lässt sich nur zur Laufzeit ausführen. a ist kein konstanter Ausdruck. Die kritische Zeile ist die Zeile (3). Die Funktion kann zur Compilezeit oder zur Laufzeit ausgeführt werden. Ob sie zur Compilezeit oder Laufzeit ausgeführt wird, hängt von dem verwendeten Compiler und dem Optimierungsstufen des Programms ab. Die Beobachtung gilt nicht für die Zeile (4). Eine consteval-Funktion wird immer zur Compilezeit ausgeführt.

In dem folgenden Programm constexprConstinit.cpp vergleiche ich const, constexpr und constint:

// constexprConstinit.cpp

#include <iostream>

constexpr int constexprVal = 1000;
constinit int constinitVal = 1000;

int incrementMe(int val){ return ++val;}

int main() {

auto val = 1000;
const auto res = incrementMe(val); // (1)
std::cout << "res: " << res << std::endl;

// std::cout << "res: " << ++res << std::endl; ERROR (2)
// std::cout << "++constexprVal++: " << ++constexprVal << std::endl; ERROR (2)
std::cout << "++constinitVal++: " << ++constinitVal << std::endl; // (3)

constexpr auto localConstexpr = 1000; // (4)
// constinit auto localConstinit = 1000; ERROR

}

Lediglich die const-Variable (1) wird zur Laufzeit initialisiert. constexpr- und constinit-Variablen werden zur Compilezeit initialisiert. constinit (3) impliziert nicht die Konstantheit der Variable wie im Falle von const (2) oder constexpr (2). Eine als constexpr (4) oder const (1) deklarierte Variable kann als eine lokale Variable erzeugt werden. Das gilt aber nicht für eine constinit-Variable.

C++20: consteval und constinit

Die Initialisierung von statischen Variablen in verschieden Übersetzungseinheiten besitzt ein ernsthaftes Problem: Es ist nicht definiert, in welcher Reihenfolge statische Variablen initialisiert werden, deren Initialisierung voneinander abhängt. Um es kurz zu machen: In meinem nächsten Artikel geht es um das Static Initialization Order Fiasco und wie sich dies dank constinit in Wohlgefallen auflöst. ( [2])


URL dieses Artikels:
https://www.heise.de/-4841497

Links in diesem Artikel:
[1] https://godbolt.org/
[2] mailto:rainer@grimm-jaud.de