constexpr und consteval Funktionen in C++20

Mit C++20 wurde constexpr viel mächtiger. Zusätzlich besitzt C++20 consteval Funktionen, die constexpr Funktionen sehr ähnlich sind.

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

Mit C++20 wurde constexpr viel mächtiger. Zusätzlich besitzt C++20 consteval Funktionen, die constexpr Funktionen sehr ähnlich sind.

Ich möchte zunächst eine Funktionalität in C++20 beschreiben, die mich wohl am meisten überrascht hat.

C++20 bietet die constexpr Container std::vector und std::string, wobei constexpr bedeutet, dass die Memberfunktionen beider Container zur Compilezeit angewendet werden können. Zusätzlich können die mehr als 100 klassischen Algorithmen der Standard Template Library als constexpr deklariert werden. Damit lässt sich ein std::vector von ints zur Compilezeit sortieren.

Schauen wir uns an, was das bedeutet:

// constexprVector.cpp

#include <algorithm>
#include <iostream>
#include <vector>

constexpr int maxElement() {
std::vector myVec = {1, 2, 4, 3}; // (1)
std::sort(myVec.begin(), myVec.end());
return myVec.back();
}
int main() {

std::cout << '\n';

constexpr int maxValue = maxElement();
std::cout << "maxValue: " << maxValue << '\n';

constexpr int maxValue2 = [] {
std::vector myVec = {1, 2, 4, 3}; // (2)
std::sort(myVec.begin(), myVec.end()) ;
return myVec.back();
}();

std::cout << "maxValue2: " << maxValue2 << '\n';

std::cout << '\n';

}

Die beiden Container std::vector ((1) und (2)) werden zur Compilezeit mithilfe von constexpr-deklarierten Funktionen sortiert. Im ersten Fall gibt die Funktion maxElement das letzte Element des Vektors myVec zurück, was dessen Maximalwert ist. Im zweiten Fall verwende ich eine direkt aufgerufene Lambda-Funktion, die als constexpr deklariert ist. Hier ist das Ergebnis des Programms:

Der entscheidende Grund für constexpr Container ist die transiente Zuweisung.

Transiente Zuweisung bedeutet, dass der zur Compilezeit zugewiesene Speicher auch zur Compilezeit wieder freigegeben werden muss. So kann der Compiler ein Missverhältnis zwischen Zuweisung und Freigabe in einer constexpr Funktion erkennen. Das folgende Beispiel wendet die transiente Zuweisung an.

// transientAllocation.cpp

#include <memory>

constexpr auto correctRelease() {
auto* p = new int[2020];
delete [] p;
return 2020;
}

constexpr auto forgottenRelease() { // (1)
auto* p = new int[2020];
return 2020;
}

constexpr auto falseRelease() { // (3)
auto* p = new int[2020];
delete p; // (2)
return 2020;
}

int main() {

constexpr int res1 = correctRelease();
constexpr int res2 = forgottenRelease();
constexpr int res3 = falseRelease();

}

Das kleine Programm hat zwei ernsthafte Probleme. Erstens wird der Speicher in der constexpr Funktion forgottenRelease (1) nicht freigegeben. Zweitens stimmt die Nicht-Array-Freigabe (3) in der constexpr Funktion falseRelease (2) nicht mit der Array-Freigabe überein. Infolgedessen schlägt die Kompilierung fehl.

Mit C++20 gibt es consteval Funktionen, die den contexpr Funktionen sehr ähnlich sind.

Oft sind Entwickler irritiert, weil sie nicht wissen, ob eine constexpr Funktion zur Laufzeit oder zur Compilezeit ausgeführt wird. Betrachten wir den folgenden Codeschnipsel.

constexpr int constexprFunction(int arg) {
return arg * arg;
}

static_assert(constexprFunction(10) == 100); // (1)
int arrayNewWithConstExpressiomFunction[constexprFunction(100)]; // (2)
constexpr int prod = constexprFunction(100); // (3)

int a = 100;
int runTime = constexprFunction(a); // (4)

int runTimeOrCompiletime = constexprFunction(100); // (5)

constexprFunction ist, wie der Name schon sagt, eine constexpr Funktion.

  1. Eine constexpr Funktion muss zur Compilezeit ausgeführt werden, wenn sie in einem constexpr Kontext verwendet wird oder das Ergebnis zur Compilezeit explizit angefordert wird. (1) und (2) sind constexpr Kontexte. (3) hingegen erfordert explizit die Funktionsausführung von constexprFuncion zur Compilezeit.
  2. Der Aufruf constexprFunction(a) (4) muss zur Laufzeit ausgeführt werden, da a kein konstanter Ausdruck ist.
  3. (5) ist der interessante Fall. Es gibt keine Anforderungen an die Ausführung der Funktion. Daher kann der Aufruf constexprFunction(100) zur Laufzeit oder zur Compilezeit ausgeführt werden. Aus Sicht des C++-Standards ist beides in Ordnung.

Im Gegensatz zu einer constexpr Funktion kann eine consteval Funktion nur zur Compilezeit ausgeführt werden.

consteval erzeugt eine sogenannte immediate Funktion.

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

Jeder Aufruf einer immediate Funktion erzeugt eine Konstante zur Compilezeit. consteval kann nicht auf Destruktoren oder Funktionen angewendet werden, die allokieren oder deallokieren. Eine consteval Funktion ist wie eine constexpr Funktion implizit inline und muss die Anforderungen an eine constexpr Funktion erfüllen.

Die Anforderungen an eine constexpr Funktion in C++14 und damit auch an eine consteval Funktion sind:

Eine consteval (constexpr) Funktion kann

  • bedingte Sprung- oder Schleifenanweisungen enthalten.
  • mehr als eine Anweisung haben.
  • constexpr Funktionen aufrufen. Eine consteval Funktion kann nur eine constexpr Funktion aufrufen, aber nicht umgekehrt.
  • built-in Datentypen als Variablen verwenden, die mit einem konstanten Ausdruck initialisiert werden müssen.

Eine consteval (constexpr) Funktion kann nicht

  • statische oder thread_local Daten besitzen
  • weder einen try-Block noch eine goto-Anweisung besitzen.
  • nicht conteval Funktionen oder nicht constexpr Daten aufrufen oder verwenden.

Es gibt einen interessanten Anwendungsfall, den consteval ermöglicht. Mit consteval lässt sich eine lokale nicht konstante Variable zur Compilezeit initialisieren.

// compileTimeInitializationLocal.cpp

consteval auto doubleMe(auto val) {
return 2 * val;
}

int main() {

auto res = doubleMe(1010); // (1)
++res; // 2021 (2)

}

Die lokale Variable res wird zur Compilezeit initialisiert (1) und zur Laufzeit geändert (2). Wenn die Funktion doubleMe hingegen als constexpr deklariert wird, könnte sie zur Laufzeit ausgeführt werden.

Bevor ich in den neuen Themenblock Design mit Templates eintauche, möchte ich im nächsten Beitrag das C++17-Feature constexpr if vorstellen. constexpr if ermöglicht es, Quellcode bedingt zu kompilieren und kann auch für nette Tricks zur Compilezeit verwendet werden. ()