constexpr std::vector und constexpr std::string in C++20

Modernes C++ Rainer Grimm  –  6 Kommentare

Das wohl am einflussreichste Schlüsselwort im modernem C++ ist constexpr. Mit C++20 erhalten wir einen constexpr std::vector und einen constexpr std::string. Zusätzlich lassen sich beide Container mit den constexpr-Algorithmen der Standard Template Library zur Compilezeit modifizieren

In diesem Artikel möchte ich die Summe und das Produkt mehrerer Zahlen zur Compilezeit berechnen. Je nachdem, welchen C++-Standard ich dabei einsetze, ist die Aufgabe anspruchsvoll und aufwendig oder geht leicht von der Hand. Der Artikel startet mit C++11.

Variadic Templates in C++11

Ein Variadic Template ist ein Template, das sich mit einer beliebigen Anzahl von Argumenten aufrufen lässt. Durch die Verwendung der Ellipse (...) wird tails zu einem Parameter-Pack. Nur zwei Operationen lassen sich auf ein Parameter-Pack anwenden: packen und entpacken. Wenn die Ellipse links von tails steht, wird gepackt, wenn sie rechts von tails steht, entpackt:

// compiletimeVariadicTemplates.cpp

#include <iostream>

template<int...>
struct sum;

template<>
struct sum<> {
static constexpr int value = 0;
};

template<int i, int... tail>
struct sum<i, tail...> {
static constexpr int value = i + sum<tail...>::value;
};

template<int...> // (1)
struct product;

template<> // (2)
struct product<> {
static constexpr int value = 1;
};

template<int i, int... tail> // (3)
struct product<i, tail...> {
static constexpr int value = i * product<tail...>::value;
};


int main() {

std::cout << std::endl;

std::cout << "sum<1, 2, 3, 4, 5>::value: " << sum<1, 2, 3, 4, 5>::value << std::endl;
std::cout << "product<1, 2, 3, 4, 5>::value: " << product<1, 2, 3, 4, 5>::value << std::endl;

std::cout << std::endl;

}

Das Programm berechnet die Summe und das Produkt der Zahlen 1 bis 5 zur Compilezeit. Im Fall des Funktions-Templates product erklärt die Zeile (1) das primäre Template, die Zeile (2) die vollständige Spezialisierung für kein Argument und die Zeile (3) die partielle Spezialisierung für zumindest ein Argument. Die Definition des primären Templates ist nicht notwendig, falls sie nicht verwendet wird. Die partielle Spezialisierung (3) startet die rekursive Instanziierung, die dann zum Ende kommt, wenn alle Argumente konsumiert sind. In diesem Fall wird die vollständige Spezialisierung für kein Argument als Endbedingung verwendet. Wenn du die Entpackung des Parameter-Packs genauer studieren möchtest, studiere das Beispiel compileTimeVariadicTemplate.cpp auf C++ Insights:

Dank Fold Expressions wird diese Berechnung deutlich einfacher.

Fold Expression in C++17

Mit C++17 können wir Parameter-Packs direkt über einem binären Operator reduzieren:

// compiletimeFoldExpressions.cpp

#include <iostream>

template<typename... Args>
auto sum(const Args&... args)
{
return (args + ...);
}

template<typename... Args>
auto product(const Args&... args)
{
return (args * ...);
}

int main() {

std::cout << std::endl;

std::cout << "sum(1, 2, 3, 4, 5): " << sum(1, 2, 3, 4, 5) << std::endl;
std::cout << "product(1, 2, 3, 4, 5): " << product(1, 2, 3, 4, 5) << std::endl;

std::cout << std::endl;

}

Das Programm compileTimeFoldExpressions.cpp liefert dieselben Ergebnisse wie das vorherige Programm compileTimeVariadicTemplates.cpp:

Natürlich gibt es mehr zu Fold Expressions in C++17 zu erzählen. Diese Details lassen sich in meinem früheren Artikel Fold Expressions nachlesen.

Jetzt will ich mich aber endlich mit C++20 beschäftigen.

constexper-Container und -Algorithmen in C++20

C++20 unterstützt die constexpr-Container std::vector und std::string. constexpr bedeutet in diesem Fall, dass die Methoden beider Container zur Compilezeit ausgeführt werden können.

Bevor ich aber über beide Container schreibe, muss ich nochmals einen kleinen Abstecher zu C++17 machen. Der Grund ist einfach: Kein Compiler unterstützt zum gegenwärtigen Zeitpunkt einen constexpr std::vector oder constexpr std::string. Im Gegensatz dazu unterstützen der GCC und der MS Compiler die constexpr-Algorithmen der STL.

In meinem folgenden Beispiel verwende ich anstelle des constexpr std::vector das constexpr std::array. Seit C++17 lässt sich ein std::array als constexpr deklarieren: constexpr std::array myArray{1, 2, 3}.

Jetzt geht der Spaß los. Mit C++20 lässt sich ein std::array zur Compilezeit verwenden:

// constexprArray.cpp

#include <iostream>
#include <numeric>
#include <array>


int main() {

std::cout << std::endl;

constexpr std::array myArray{1, 2, 3, 4, 5}; // (1)
constexpr auto sum = std::accumulate(myArray.begin(), myArray.end(), 0); // (2)
std::cout << "sum: " << sum << std::endl;

constexpr auto product = std::accumulate(myArray.begin(), myArray.end(), 1, // (3)
std::multiplies<int>());
std::cout << "product: " << product << std::endl;

constexpr auto product2 = std::accumulate(myArray.begin(), myArray.end(), 1, // (4)
[](auto a, auto b) { return a * b;});
std::cout << "product2: " << product2 << std::endl;

std::cout << std::endl;

}

Das std::array (1) und alle Ergebnisse der Berechnungen sind als constexpr deklariert. Zeile (2) berechnet die Summe aller Elemente und die Zeilen (3) und (4) das Produkt aller Elemente von myArrray. Die Zeile (2) ist gültig, da myArray ein constexpr-Container und der Algorithmus std::accumulate als constexpr deklariert ist. Die Zeilen (3) und (4) sind deutlich interessanter. Der Klammeroperator von std::multiplies ist constexpr deklariert und seit C++17 können Lambda-Ausdrücke constexpr sein.

Hier ist die Ausgabe des Programms:

Dank des Compiler Explorer kann ich die Ergebnisse der Berechnung deutlich beeindruckender präsentieren. Dies sind die entscheidenden Assembler-Instruktionen mit dem GCC:

Die Zeilen 19, 29 und 39 zeigen, dass die Ergebnisse der Array-Berechnungen Werte in den Assember-Instruktionen sind. Das heißt, dass std::accumulate zur Compilezeit ausgeführt und die Ergebnisse zur Laufzeit vorhanden sind.

Wie ich bereits geschrieben habe, unterstützt zum jetzigen Zeitpunkt kein Compiler einen constexpr std::vector oder std::string. Daher muss ich jetzt ein wenig schummeln und annehmen, dass mein Compiler beide constexpr-Container vollständig unterstützt:

// constexprVectorString.cpp

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

int main() {

std::cout << std::endl;

constexpr std::vector myVec {15, -5, 0, 5, 10};
constexpr std::sort(myVec.begin(), myVec.end());
for (auto v: myVec) std::cout << v << " ";
std::cout << "\n\n";

using namespace std::string_literals;
constexpr std::vector<std::string> myStringVec{"Stroustrup"s, "Vandevoorde"s,
"Sutter"s, "Josuttis"s, "Wong"s };
constexpr std::sort(myStringVec.begin(), myStringVec.end());
for (auto s: myStringVec) std::cout << s << " ";

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

}

Mit C++20 lässt sich ein std::vector oder ein std::string zur Compilezeit sortieren:

Wie geht's weiter?

C++20 bietet zusätzlich viele Funktionen an, die den Umgang mit Containern der Standard Template Library deutlich angenehmer machen. Dank den Funktionen std::erase und std::erase_if geht das Löschen der Elemente eines Containers deutlich leichter von der Hand. Mit der Funktion contains ist es einfach zu bestimmen, ob ein bestimmtes Element in einem assoziativen Container enthalten ist.