C++20: Concepts definieren

Mit diesem Artikel werde ich mir das letzte interessante Thema zu Concepts genauer anschauen: die Definition von Concepts. Natürlich beantworte ich in dem Zuge die Fragen, die meine vorherigen Artikel offen gelassen haben.

In Pocket speichern vorlesen Druckansicht 12 Kommentare lesen
Lesezeit: 7 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Mit diesem Artikel werde ich mir das letzte interessante Thema zu Concepts genauer anschauen: die Definition von Concepts. Außerdem beantworte ich in dem Zuge die Fragen, die meine vorherigen Artikel offen gelassen haben.

Zuerst einmal sind die meisten von mir definierten Concepts bereits im C++20-Standard enthalten. Daher gibt es keinen Grund, diese nochmals zu implementieren. Um meine Concepts von denen des C++20-Standards zu unterscheiden, habe ich sie groß geschrieben. Zur Erinnerung: Mein vorheriger Artikel stellte die vordefinierten Concepts bereits dar: C++20: Vordefinierte Concepts.

Es gibt zwei typische Wege, Concepts zu definieren: die direkte Definition und die sogenannten requires-expressions lassen sich anwenden.

Die Syntax der direkten Definition im Standard hat sich ein wenig zur Syntax des Concepts TS (Technical Specification) geändert.

  • Concepts TS
template<typename T>
concept bool Integral(){
return std::is_integral<T>::value;
}
  • C++20-Standard
template<typename T>
concept Integral = std::is_integral<T>::value;

Die Syntax des C++20-Standards ist kompakter. Beide Implementierungen verwenden unter der Haube die Funktion std::is_integral<T>::value aus der Typ-Traits-Bibliothek von C++11. T erfüllt das Concept genau dann, wenn das Compilezeit-Prädikat std::is_integral<T>::value zu true evaluiert. Compilezeit-Prädikat bedeutet, dass die Funktion zur Compilezeit ausgeführt wird und einen Wahrheitswert zurückgeben muss. Seit C++17 lässt sich der Ausdruck std::is_integral<T>::value einfacher schreiben: std::is_integral_v<T>.

Ich bin mir nicht sicher, ob die zwei Begriffe variable concept für die direkte Definition und function concept für die requires-expressions noch verwendet werden. Beide Begriffe helfen aber, die direkte Definition von requires-expressions zu unterscheiden.

Ich lasse das Beispiel zur Verwendung des Concepts Integral aus. Falls du neugierig bist, lies meinen vorherigen Artikel: C++20: Concepts – die Placeholder-Syntax.

Entsprechend der direkten Definition ändert sich die Syntax der requires-expressions von der Syntax des Concepts TS zu der des C++20-Standards.

  • Concepts TS
template<typename T>
concept bool Equal(){
return requires(T a, T b) {
{ a == b } -> bool;
{ a != b } -> bool;
};
}
  • C++20-Standard
template<typename T>
concept Equal =
requires(T a, T b) {
{ a == b } -> bool;
{ a != b } -> bool;
};

Wie zuvor ist die Syntax des C++20-Standards kompakter. T erfüllt das Concept genau dann, wenn die Operatoren == und != überladen sind und einen Wahrheitswert zurückgeben. Zusätzlich müssen die Datentypen von a und b identisch sein.

Jetzt ist es an der Zeit, das Concept Equal anzuwenden:

// conceptsDefintionEqual.cpp

#include <iostream>

template<typename T>
concept Equal =
requires(T a, T b) {
{ a == b } -> bool;
{ a != b } -> bool;
};


bool areEqual(Equal auto a, Equal auto b) { // (1)
return a == b;
}

/*

struct WithoutEqual{
bool operator==(const WithoutEqual& other) = delete;
};

struct WithoutUnequal{
bool operator!=(const WithoutUnequal& other) = delete;
};

*/

int main() {

std::cout << std::boolalpha << std::endl;

std::cout << "areEqual(1, 5): " << areEqual(1, 5) << std::endl;

/*

bool res = areEqual(WithoutEqual(), WithoutEqual());

bool res2 = areEqual(WithoutUnequal(), WithoutUnequal());

*/

std::cout << std::endl;

}

Ich nutze das Concept Equal in der Funktion areEqual (Zeile 1). Zur Erinnerung, durch die Verwendung eines Concepts als Funktionsparameter erzeugt der Compiler ein Funktions-Template, für dessen Argumente die Einschränkungen des Concepts gelten. Mehr Informationen zu dieser einfachen Art, Concepts anzuwenden, gibt es wiederum in meinem vorherigen Artikel: C++20: Concepts – die Placeholder-Syntax.

Die Ausgabe des Programms ist unspektakulär:

Jetzt wird es aber spannender. Was passiert, wenn ich die Datentypen WithoutEqual und WithoutUnequal verwende? Natürlich habe ich absichtlich die Operatoren == und != auf delete gesetzt. Der Compiler beschwert sich postwendend, dass beide Datentypen nicht das Concept Equal unterstützen.

Wenn du die Fehlermeldung sorgfältig studierst, sollte der Grund klar sein: (a == b) would be ill-formed und (a != b) would be ill-formed.

Bevor ich den Artikel fortsetze, möchte ich einen kleinen Ausflug einlegen. Er ist notwendig, um den Sourcecode zu kompilieren.

Ich habe bei der Ausgabe des Programms conceptsDefinitionEqual.cpp ein wenig geschummelt. Die Ausgabe stammt von der Concepts-TS-Implementierung des GCC. Zum jetzigen Zeitpunkt gibt es keine Implementierung, die standardkonform zur C++20-Syntax der Concepts ist.

  • Der neueste Microsoft-Compiler setzt zwar die C++20-Syntax für Concepts um, kann aber noch nicht die Placeholer-Syntax, die ich für die Funktion areEqual verwendet habe.
  • Der GCC-Compiler unterstützt zwar die Placeholder-Syntax, hingegen nicht die C++20-Syntax, Concepts zu definieren.

Ich habe bereits in einem älteren Artikel geschrieben (C++20: Zwei Extreme und die Rettung dank Concepts), dass mich zu Anfang Concepts an Haskells Typklassen erinnerten. Typklassen in Haskell sind Interfaces für ähnliche Datentypen. Der Hauptunterschied zu Concepts ist aber, dass ein Datentyp wie Int in Haskell eine Instanz einer Typklasse sein und daher die Typklasse implementieren muss. Im Gegensatz dazu, prüft der Compiler bei Concepts, ob ein Datentyp ein Concept erfüllt.

Dies ist ein Ausschnitt aus der Typklassen-Hierarchie Haskells:

Hier ist meine entscheidende Beobachtung: Haskell unterstützt die Typklasse Eq. Wenn du die Definition der Typklasse Eq mit dem Concept Equal vergleichst, sind sie sehr ähnlich:

class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
template<typename T>
concept Equal =
requires(T a, T b) {
{ a == b } -> bool;
{ a != b } -> bool;
};

Haskells Typklasse fordert von einer Instanz wie Int:

  • Der gleich- (==) und ungleich-Operator (/=) muss unterstützt werden und dieser einen Wahrheitswert zurückgeben.
  • Die Operanden beider Operatoren müssen den gleichen Datentyp (a -> a) besitzen.

Gerne möchte ich nochmals genauer die Typklassen-Hierarchie Haskells anschauen. Die Typklasse Ord ist eine Verfeinerung der Typklasse Eq. Dies geht auch aus der Definition der Typklasse Ord hervor:

class Eq a => Ord a where
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a

Der interessanteste Punkt der Definition der Typklasse Ord ist ihre erste Zeile. Eine Instanz der Typklasse Ord muss bereits eine Instanz der Typklasse Eq sein. Ordering ist eine Aufzählung mit den Werten EQ, LT und GT. Diese Verfeinerung einer bestehenden Typklasse in Haskell finde ich sehr elegant.

Dies ist meine Herausforderung für meinen nächsten Artikel zu Concepts: Lassen sich Concepts ähnlich elegant verfeinern wie Typklassen in Haskell?

In meinem nächsten Artikel nehme ich die Herausforderung an, das Concept Equal zu verfeinern. Zusätzlich werde ich die zwei wichtigen Concepts Regular und Semiregular genauer vorstellen und implementieren.


()