Zwei neue Schlüsselwörter in C++20: consteval und constinit
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.
- Rainer Grimm
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.
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
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
- bedingte Sprung- und Iterationsanweisungen enthalten.
- mehrere Anweisungen umfassen.
constexpr
-Funktionen aufrufen. Eineconsteval
-Funktion kann eineconstexpr
-Funktion aufrufen. Eineconstexpr
-Funktion kann aber nicht eineconsteval
-Funktion aufrufen.- fundamentale Datentypen verwenden, die mit einem konstanten Ausdruck zu initialisieren sind.
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 kann ich das Programm ausführen.
constinit
constinit
kann auf Variablen mit statischer Speicherdauer (static storage duration) oder Thread-Speicherdauer (thread storage duration) angewandt werden.
- Globale oder statische Variablen oder statische Mitglieder einer Klasse besitzen statische Speicherdauer. Diese Objekte werden allokiert, wenn das Programm startet, und deallokiert, wenn das Programm endet.
thread_loca
l-Variablen besitzen Thread-Speicherdauer. Thread-lokale Daten werden bei Bedarf für jeden Thread erzeugt. Sie gehören exklusiv einem Thread, werden bei ihrer ersten Verwendung erzeugt und ihre Lebenszeit ist an die Lebenszeit ihres Threads gebunden. Gerne werden Thread-lokale Daten auch Thread-lokaler Speicher genannt.
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.
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.
Ausführen einer Funktion
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.
Initialisierung einer Variable
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.
Wie geht's weiter?
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.
()