- Allgemeines
- Variante 1:
requires-Klausel zwischen der Template-Parameterliste und dem Rückgabetyp der Funktion - Variante 2: Nachgestellte
requires-Klausel - Variante 3: Template-Parameter mit Einschränkungen („Constraints”)
- Variante 4: Abgekürzte Funktionstemplates („Abbreviated Function Templates”)
requires-Klauseln mit benutzerdefinierten Einschränkungen- Literaturhinweise
Templates eignen sich sehr gut zum Schreiben von Code, der mit unterschiedlichen Datentypen funktioniert.
Mit dem Konzept von „C++ Concepts” wiederum können die Parameter für Templates durch boolesche Prädikate validiert werden.
Auf diese Weise kann man erreichen, dass Funktionstemplates nur für sinnvolle Template Parameter instanziiert werden.
Bei der ersten der vier vorgestellten Möglichkeiten verwenden wir die
requires-Klausel zwischen der Template-Parameterliste und dem Rückgabetyp der Funktion.
Der Rückgabetyp der Funktion kann dann auto oder einer der Template Parameter sein.
Beispiel:
01: template <typename T>
02: concept Numerical = std::integral<T> or std::floating_point<T>;
03:
04: template <typename T>
05: requires Numerical<T>
06: auto add(T a, T b)
07: {
08: return a + b;
09: }Natürlich kann ein Funktionstemplate mehrere Template Parameter haben. Im nachfolgenden Beispiel lassen sich Werte unterschiedlichen Typs addieren:
template <typename T, typename U>
requires Numerical<T> and Numerical<U>
auto add(T a, U b)
{
return a + b;
}Das Schlüsselwort requires ist neu ab C++ 20.
Es wendet Einschränkungen (Constraints) auf ein Template
bzw. auf die Parameter eines Templates an.
Numerical ist der Name eines Konzepts, das nur Integer- und Gleitkommatypen akzeptiert.
Versucht man, derartige Templates mit unzulässigen Datentyp Parametern zu übersetzen, erhält man verständliche Übersetzungsfehler:
The concept 'Numerical<std::string>' evaluated to false
The associated constraints are not satisfied
Man kann die Requirements der Template Parameter auch in einer Art
„inline”–Notation formulieren. In diesem Fall entfällt die separate
concept-Definition:
01: template <typename T>
02: requires std::integral<T> or std::floating_point<T>
03: auto add(T a, T b)
04: {
05: return a + b;
06: }Zur Nomenklatur:
Der C++–Standard verwendet die Begriffe Konjunktion, Disjunktion und atomar,
um Arten von Ausdrücken zu beschreiben, die zum Erstellen einer requires-Klausel / eines Konzepts
verwendet werden können:
-
Eine Einschränkungs-Konjunktion (constraint conjunction) wird gebildet, indem der
&&-Operator (and-Operator) auf zwei Einschränkungen angewendet wird. -
Eine Einschränkungs-Disjunktion (constraint disjunction) wird gebildet, indem der
||-Operator (or-Operator) auf zwei Einschränkungen angewendet wird. -
Eine atomare Einschränkung bezeichnet einen Ausdruck, der einen boolschen Typ zurückgibt und nicht weiter zerlegt werden kann.
-
Der logische Not-Operator (
!,not) wird verwendet, um den negierten Wert einer Einschränkung zu erhalten.
Das Schlüsselwort requires tritt in dieser Variante in der Funktionssignatur
in Erscheinung:
Beispiel:
01: template <typename T>
02: auto add(T a, T b) requires Numerical<T>
03: {
04: return a + b;
05: }Wir erhalten das gleiche Ergebnis wie bei der requires-Klausel zuvor,
wir haben die Klausel nur mit einer anderen Syntax geschrieben.
In dieser Definition können wir wieder nicht zwei Zahlen unterschiedlichen Typs addieren. Wir müssten die Template Definition ähnlich wie zuvor ändern:
01: template <typename T, typename U>
02: auto add(T a, U b) requires Numerical<T> and Numerical<U>
03: {
04: return a + b;
05: }Die dritte Art, ein Konzept zu verwenden, ist in der Schreibweise etwas knapper als die vorherigen. Der Bezeichner des Konzepts wird in der Template Deklaration verwendet:
Beispiel:
01: template <Numerical T>
02: auto add(T a, T b)
03: {
04: return a + b;
05: }In dieser Schreibweise kann man erkennen, das es keine requires-Klausel mehr gibt.
Der Template Parameter – in der Regel T – wird anstelle mit
dem Schlüsselwort typename jetzt mit einem Bezeichner für ein Konzept (hier: Numerical) definiert.
Wir erzielen das gleiche Ergebnis wie mit den beiden vorherigen Varianten.
Die dritte Variante besitzt einige Einschränkungen.
Wenn man die requires-Klausel wie in der Art von Variante 1 oder 2 verwenden wollte,
könnte man einen Ausdruck wie z.B.
requires std::integral<T> or std::floating_point<T>schreiben. In der dritten Variante geht das nicht.
Für all diejenigen, die es kurz lieben, gibt es die so genannte „Abbreviated Function Templates” Schreibweise:
01: auto add(Numerical auto a, Numerical auto b)
02: {
03: return a + b;
04: }Wenn Sie sich für die „Abbreviated Function Templates” Schreibweise entscheiden,
können Sie sowohl auf Template Parameter als auch auf requires-Klauseln verzichten.
Sie können concept-Bezeichner direkt dort verwenden,
wo die Funktionsargumente aufgelistet sind.
Beachte:
Der concept-Bezeichner wird in dieser Variante ohne Template Parameter verwendet!
Zum Schreiben einer requires-Klausel lassen sich die vordefinierten
Merkmale („Traits”) aus dem Header <type_traits> verwenden.
Man kann aber auch seine eigenen Merkmale definieren.
Diese müssen dann den Typ constexpr bool zurückgeben.
Beispiel:
template<typename T>
constexpr bool isGreaterThanWord{ sizeof(T) > 2 };Damit ließe sich dann ein Konzept so definieren:
// using <type_traits>
template <typename T>
concept GreatIntegral = std::is_integral<T>::value and isGreaterThanWord<T>;Eine mögliche Anwendung könnte dann so aussehen:
template<GreatIntegral T>
T incrementByOne(const T& arg) {
return arg + 1;
}Ideen und Anregungen zu den Beispielen aus diesem Abschnitt stammen aus
4 ways to use C++ Concepts in Functions (abgerufen am 09.04.2023).
von Sandor Dargo.