Requires Expressions in C++20 direkt verwenden
Requires Expressions in C++20 lassen sich auch als eigenständiges Feature verwenden, wenn ein Prädikat zur Compilezeit erforderlich ist.
![](https://heise.cloudimg.io/width/610/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/5/3/0/7/9/0/TimelineCpp20Concepts-a060a9a4c20aa895.png)
- Rainer Grimm
In meinem letzten Artikel "Concepts mit Requires Expressions definieren" habe ich gezeigt, wie sich mit Requires Expressions Concepts definieren lassen. Requires Expressions können aber auch als eigenständiges Feature verwendet werden, wenn ein Prädikat zur Compilezeit erforderlich ist.
Typische Anwendungsfälle für Compilezeit-Prädikate sind static_assert
, constexpr if
oder eine Requires Clause. Ein Compilezeit-Prädikat ist ein Ausdruck, der zur Compilezeit einen booleschen Wert zurückgibt. Ich beginne diesen Artikel mit dem C++11 static_assert
.
static_assert
static_assert
benötigt ein Compilezeitprädikat und eine Nachricht, die angezeigt wird, wenn das Compilezeitprädikat fehlschlägt. Mit C++17 ist die Nachricht optional. Mit C++20 kann dieses Compilezeitprädikat ein Requires Expression sein.
// staticAssertRequires.cpp
#include <concepts>
#include <iostream>
struct Fir { // (4)
int count() const {
return 2020;
}
};
struct Sec {
int size() const {
return 2021;
}
};
int main() {
std::cout << '\n';
Fir fir;
static_assert(requires(Fir fir){ { fir.count() } -> std::convertible_to<int>; }); // (1)
Sec sec;
static_assert(requires(Sec sec){ { sec.count() } -> std::convertible_to<int>; }); // (2)
int third;
static_assert(requires(int third){ { third.count() } -> std::convertible_to<int>; }); // (3)
std::cout << '\n';
}
Die Requires Expressions (Zeilen 1, 2 und 3) prüfen, ob das Objekt eine Memberfunktion count
hat und ihr Ergebnis in int
konvertierbar ist. Diese Prüfung ist nur für die Klasse First
(Zeile 4) möglich. Die Prüfungen in den Zeilen (2) und (3) schlagen dagegen fehl.
Vielleicht möchtest du Code in Abhängigkeit von einer Compilezeitprüfung kompilieren? In diesem Fall ist das C++17-Feature constexpr if
in Kombination mit Requires Expressions das passende Werkzeug.
constexpr if
constexpr if
ermöglicht es, Quellcode bedingt zu kompilieren. Für die Bedingung kommt die Requires Expression ins Spiel. Alle Zweige der if
-Anweisung müssen gültig sein.
Dank constexpr if
kannst du Funktionen definieren, die ihre Argumente zur Compilezeit untersuchen und auf der Grundlage ihrer Analyse unterschiedliche Funktionalität erzeugen.
// constexprIfRequires.cpp
#include <concepts>
#include <iostream>
struct First {
int count() const {
return 2020;
}
};
struct Second {
int size() const {
return 2021;
}
};
template <typename T>
int getNumberOfElements(T t) {
if constexpr (requires(T t){ { t.count() } -> std::convertible_to<int>; }) {
return t.count();
}
if constexpr (requires(T t){ { t.size() } -> std::convertible_to<int>; }) {
return t.size();
}
else return 42;
}
int main() {
std::cout << '\n';
First first;
std::cout << "getNumberOfElements(first): " << getNumberOfElements(first) << '\n';
Second second;
std::cout << "getNumberOfElements(second): " << getNumberOfElements(second) << '\n';
int third;
std::cout << "getNumberOfElements(third): " << getNumberOfElements(third) << '\n';
std::cout << '\n';
}
Die Zeilen (1) und (2) sind in diesem Codebeispiel entscheidend. In Zeile (1) bestimmen die Requires Expressions, ob die Variable t
eine Memberfunktion count
hat, die einen int
zurückgibt. Entsprechend wird in Zeile (2) festgestellt, ob die Variable t
eine Memberfunktion size
besitzt. Die else
-Anweisung in Zeile (3) kommt als Fallback zum Einsatz.
Requires Clause
Zuallererst muss ich die Frage beantworten: Was ist eine Requires Clause?
Es gibt im Wesentlichen vier Möglichkeiten, ein Concept wie std::integral
zu verwenden.
// conceptsIntegralVariations.cpp
#include <concepts>
#include <type_traits>
#include <iostream>
template<typename T> // (1)
requires std::integral<T>
auto gcd(T a, T b) {
if( b == 0 ) return a;
else return gcd(b, a % b);
}
template<typename T> // (2)
auto gcd1(T a, T b) requires std::integral<T> {
if( b == 0 ) return a;
else return gcd1(b, a % b);
}
template<std::integral T> // (3)
auto gcd2(T a, T b) {
if( b == 0 ) return a;
else return gcd2(b, a % b);
}
auto gcd3(std::integral auto a, // (4)
std::integral auto b) {
if( b == 0 ) return a;
else return gcd3(b, a % b);
}
int main(){
std::cout << '\n';
std::cout << "gcd(100, 10)= " << gcd(100, 10) << '\n';
std::cout << "gcd1(100, 10)= " << gcd1(100, 10) << '\n';
std::cout << "gcd2(100, 10)= " << gcd2(100, 10) << '\n';
std::cout << "gcd3(100, 10)= " << gcd3(100, 10) << '\n';
std::cout << '\n';
}
Dank dem Header <concepts>
kann ich das Concept std::integral
verwenden. Das Concept ist erfüllt, wenn T
integral ist. Der Funktionsname gcd
steht für den greatest-common-divisor-Algorithmus, der auf dem euklidischen Algorithmus basiert.
Hier sind die vier Möglichkeiten, Concepts zu verwenden:
- Requires Clause (Zeile 1)
- Trailing Requires Clause (Zeile 2)
- Constrained Template Parameter (Zeile 3)
- Abbreviated Function Template (Zeile 4)
Der Einfachheit halber gibt jedes Funktions-Template auto
zurück. Es gibt einen semantischen Unterschied zwischen den Funktions-Templates gcd, gcd1, gcd2
und der Funktion gcd3
. Im Fall von gcd, gcd1
oder gcd2
müssen die Argumente a
und b
denselben Typ besitzen. Das gilt nicht für die Funktion gcd3
. Die Parameter a
und b
können unterschiedliche Typen haben, müssen aber beide das Concept std::integral
erfüllen.
Die Funktionen gcd
und gcd1
verwenden Requires Clauses.
Es gibt eine interessante Eigenschaft von Requires Clauses. Du kannst jedes Compilezeit-Prädikat als Ausdruck verwenden. In der folgenden Requires Clause prüfe ich, ob ein int
als Nicht-Typ-Template-Parameter kleiner als 20 ist.
// requiresClause.cpp
#include <iostream>
template <unsigned int i>
requires (i <= 20)
int sum(int j) {
return i + j;
}
int main() {
std::cout << '\n';
std::cout << "sum<20>(2000): " << sum<20>(2000) << '\n';
// std::cout << "sum<23>(2000): " << sum<23>(2000) << '\n'; // ERROR
std::cout << '\n';
}
Das in Zeile (1) verwendete Compilezeitprädikat verdeutlicht einen interessanten Punkt: Die Anforderung wird auf den Nicht-Typ i
angewendet und nicht wie üblich auf einen Typ.
Wenn du die auskommentierte Zeile im Hauptprogramm verwendest, meldet der Clang-Compiler den folgenden Fehler:
Hier gibt es weitere Informationen über Nicht-Typ-Template-Parameter: "Alias Templates und Template Parameter".
Normalerweise verwendest du ein Concept in einer Requires Clause, aber es gibt weitere Anwendungsfälle: requires requires
oder anonyme Concepts.
requires requires oder anonyme Concepts
Ein anonymes Concept lässt sich definieren und direkt verwenden. In der Regel solltest du das aber nicht tun. Mit anonymen Concepts wird der Code schwerer lesbar und das Concept lässt sich nicht wiederverwenden.
template<typename T>
requires requires (T x) { x + x; }
auto add(T a, T b) {
return a + b;
Das Funktions-Template definiert sein Concept ad-hoc. Das Funktions-Template add
verwendet eine Requires Expression (requires(T x) { x + x; }
) innerhalb einer Requires Clause. Das anonyme Concept ist äquivalent zu dem folgenden Concept Addable
.
template<typename T>
concept Addable = requires (T a, T b) {
a + b;
};
Folglich sind die folgenden vier Implementierungen des Funktions-Template add
äquivalent zu der vorherigen:
template<typename T> // requires clause
requires Addable<T>
auto add(T a, T b) {
return a + b;
}
template<typename T> // trailing requires clause
auto add(T a, T b) requires Addable<T> {
return a + b;
}
template<Addable T> // constrained template parameter
auto add(T a, T b){
return a + b;
}
// abbreviated function template
auto add(Addable auto a, Addable auto b) {
return a + b;
}
Zur Erinnerung: Die letzte Implementierung, die auf der Abbriviated Function Template Syntax basiert, kann mit Werten unterschiedlichen Typs umgehen.
Ich möchte es noch einmal betonen: Concepts sollten allgemeine Ideen umsetzen und du solltest ihnen einen selbsterklärenden Namen geben, damit sie wiederverwendet werden können. Sie sind von unschätzbarem Wert für das Verständnis von Code. Anonyme Concepts verhalten sich wie syntaktische Einschränkungen für Template Parameter und sollten vermieden werden.
Wie geht's weiter?
Die Verwendung eines Concepts in static_assert(Concept<T>)
ist im Wesentlichen ein Test, ob der Typ T
Concept erfüllt. Im nächsten Artikel zeige ich, wie sich diese Idee anwenden lässt.
()