Concepts mit Requires Expressions definieren
Neben anderen Methoden lassen sich Requires Expressions zum Definieren von Concepts verwenden.
- Rainer Grimm
In meinem letzten Artikel "Definition von Concepts" habe ich die Concepts Integral
, SignedIntegral
und UnsigendIntegral
mithilfe von logischen Kombinationen bestehender Concepts und Compile-Zeit-Prädikaten definiert. Heute wende ich Requires Expressions an.
Bevor ich auf die Verwendung von Requires Expressions zur Definition eines Concepts eingehe, hier eine kurze Erinnerung.
Die Syntax zur Definition eines Concepts ist recht einfach:
template <template-parameter-list>
concept concept-name = constraint-expression;
Die Definition eines Concepts beginnt mit dem SchlĂĽsselwort template
und hat eine Template-Parameterliste. Sie verwendet das SchlĂĽsselwort concept
gefolgt vom dem concept-name
und dem constraint-expression
.
Ein constraint-expression
ist ein Compile-Zeit-Prädikat: eine Funktion, die zur Compile-Zeit ausgeführt wird und einen booleschen Wert zurückgibt. Dieses Compile-Zeit-Prädikat kann folgende Form besitzen:
- Eine logische Kombination aus anderen Concepts oder Compile-Zeit-Prädikaten unter Verwendung von Konjunktionen (
&&
), Disjunktionen (||
) oder Negationen (!
). Ăśber die syntaktische Form habe ich bereits in meinem vorherigen Artikel "Definition von Concepts" geschrieben.
- Eine Requires Expression kann folgende Formen besitzen:
- Simple requirements
- Type requirements
- Compound requirements
- Nested requirements
Requires Expressions
Dank der Requires Expression kann man mächtige Concepts definieren. Ein Requires Expression besitzt folgende Syntax:
requires (parameter-list(optional)) {requirement-seq}
-
parameter-list
: Eine kommaseparierte Liste von Parametern wie in einer Funktionsdeklaration requirement-seq
: Eine Folge von Anforderungen, die aus einfachen, typbezogenen, zusammengesetzten oder verschachtelten Anforderungen bestehen
Requires Expressions können auch als eigenständige Features verwendet werden, wenn ein Prädikat zur Compile-Zeit erforderlich ist. Über diese Features werde ich im nächsten Artikel schreiben.
Simple Requirements
Das folgende Concept Addable
ist eine einfache Anforderung:
template<typename T>
concept Addable = requires (T a, T b) {
a + b;
};
Das Concept Addable
verlangt, dass die Addition a + b von zwei Werten desselben Typs T
möglich ist.
Bevor ich mit den Typanforderungen fortfahre, möchte ich hinzufügen: Dieses Concept hat nur einen Zweck: einfache Anforderungen zu veranschaulichen. Ein Concept zu schreiben, das einen Typ nur daraufhin überprüft, ob er den Operator +
unterstĂĽtzt, ist schlecht. Ein Concept sollte eine Idee modellieren, wie zum Beispiel die der Arithmetik.
Type Requirements
In einer Typanforderung muss man das SchlĂĽsselwort typename
zusammen mit einem Datentyp verwenden.
template<typename T>
concept TypeRequirement = requires {
typename T::value_type;
typename Other<T>;
};
Das Concept TypeRequirement
fordert, dass der Typ T
ein Member value_type
hat und dass das Klassen-Template Other
mit T
instanziiert werden kann.
Hier kommt der praktische Einsatz:
#include <iostream>
#include <vector>
template <typename>
struct Other;
template <>
struct Other<std::vector<int>> {};
template<typename T>
concept TypeRequirement = requires {
typename T::value_type; // (2)
typename Other<T>; // (3)
};
int main() {
TypeRequirement auto myVec= std::vector<int>{1, 2, 3}; // (1)
}
Der Ausdruck TypeRequirement auto myVec = std::vector<int>{1, 2, 3}
(1) ist gĂĽltig. Ein std::vector
hat ein inneres Mitglied value_type
(2) und das Klassen-Template Other
(2) kann mit std::vector<int>
(3) instanziiert werden.
Die Verwendung des Concepts TypeRequirement
in Kombination mit auto
in (1) wird als eingeschränkter Platzhalter bezeichnet. Mehr über eingeschränkte und nicht eingeschränkte Platzhalter erfährst du in meinem vorherigen Artikel "C++20: Concepts, die Placeholder Syntax".
Compound Requirements
Eine zusammengesetzte Anforderung hat folgende Form:
{expression} noexcept(optional) return-type-requirement(optional);
Zusätzlich zu einer einfachen Anforderung kann eine zusammengesetzte Anforderung einen noexcept
-Spezifizierer und eine Anforderung an den RĂĽckgabetyp enthalten. Mit dem noexcept
-Spezifizierer drückt man im Wesentlichen aus, dass dieser Ausdruck keine Ausnahme auslöst. Wenn er doch eine auslöst, soll das Programm einfach abstürzen. Mehr über den noexcept
-Specifier erfährst du in meinem Artikel: "C++ Core Guidelines: Der noexcept-Spezifizierer und -Operator".
Das Concept Equal
, das im folgenden Beispiel gezeigt wird, verwendet zusammengesetzte Anforderungen.
// conceptsDefinitionEqual.cpp
#include <concepts>
#include <iostream>
template<typename T> // (1)
concept Equal = requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
};
bool areEqual(Equal auto a, Equal auto b){
return a == b;
}
struct WithoutEqual{ // (2)
bool operator==(const WithoutEqual& other) = delete;
};
struct WithoutUnequal{ // (3)
bool operator!=(const WithoutUnequal& other) = delete;
};
int main() {
std::cout << std::boolalpha << '\n';
std::cout << "areEqual(1, 5): " << areEqual(1, 5) << '\n';
/*
bool res = areEqual(WithoutEqual(), WithoutEqual()); // (4)
bool res2 = areEqual(WithoutUnequal(), WithoutUnequal());
*/
std::cout << '\n';
}
Das Concept Equal
(1) fordert, dass sein Typ-Parameter T
die Operatoren Gleich und Nicht-Gleich unterstĂĽtzt. AuĂźerdem mĂĽssen beide Operatoren einen Wert zurĂĽckgeben, der in einen booleschen Wert konvertierbar ist. NatĂĽrlich unterstĂĽtzt int
das Concept Equal
, aber das gilt nicht fĂĽr die Datentypen WithoutEqua
l (2) und WithoutUnequal
(3). Wenn ich also den Typ WithoutEqual
(4) verwende, erhalte ich beim GCC-Compiler folgende Fehlermeldung:
Nested Requirements
Eine verschachtelte Anforderung besitzt die Form
requires constraint-expression;
Sie kommen zum Einsatz, um Anforderungen an Typ-Parameter festzulegen.
In meinem vorherigen Artikel "Definition von Concepts" habe ich das Concept UnsigendIntegral
mithilfe von logischen Kombinationen bestehender Concepts und Compile-Zeit-Prädikaten definiert. Nun definiere ich es mit verschachtelten Anforderungen:
// nestedRequirements.cpp
#include <type_traits>
template <typename T>
concept Integral = std::is_integral<T>::value;
template <typename T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;
// template <typename T> // (2)
// concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
template <typename T> // (1)
concept UnsignedIntegral = Integral<T> &&
requires(T) {
requires !SignedIntegral<T>;
};
int main() {
UnsignedIntegral auto n = 5u; // works
// UnsignedIntegral auto m = 5; // compile time error,
// 5 is a signed literal
}
(1) verwendet das Concept SignedIntegral
als verschachtelte Anforderung, um das Concept Integral
zu verfeinern. Ehrlich gesagt, ist das auskommentierte Concept UnsignedIntegral
in (2) einfacher zu lesen.
Wie geht's weiter?
Normalerweise verwendet man Requires Expressions, um ein Concept zu definieren. Sie können auch als eigenständige Features zum Einsatz kommen, wenn ein Prädikat zur Compile-Zeit erforderlich ist. Daher kann eine Requires Expression auch in einer Requires Clause, in static_assert
oder in constexpr if
verwendet werden. Ich werde in meinem nächsten Artikel über diese speziellen Anwendungsfälle von Requires Expressions schreiben.
()