Concepts mit Requires Expressions definieren

Neben anderen Methoden lassen sich Requires Expressions zum Definieren von Concepts verwenden.

Lesezeit: 5 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 1 Beitrag
Von
  • 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

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.

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.

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".

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 WithoutEqual (2) und WithoutUnequal (3). Wenn ich also den Typ WithoutEqual (4) verwende, erhalte ich beim GCC-Compiler folgende Fehlermeldung:

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.

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. ()