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.
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 [1].
![C++20: Concepts definieren](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/8/2/7/2/5/9/TimelineCpp20Concepts-fbf175a60ae13e99.png)
Concepts definieren
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 [2].
requires-expressions
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.
Das Concept Equal
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 [3].
Die Ausgabe des Programms ist unspektakulär:
![C++20: Concepts definieren](https://heise.cloudimg.io/width/455/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/8/2/7/2/5/9/conceptsDefinitionEqual-713a6fefcf00460c.png)
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.
![C++20: Concepts definieren](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/8/2/7/2/5/9/conceptsDefinitionEqualError-7ac3bfd9acf9302f.png)
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.
Der Implementierungsstatus der Concepts
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.
From Equal to Ord
Ich habe bereits in einem älteren Artikel geschrieben (C++20: Zwei Extreme und die Rettung dank Concepts [4]), 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:
![C++20: Concepts definieren](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/8/2/7/2/5/9/haskellsTypeclasses-545c60fb8ca6eb21.png)
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:
Die Typklasse Eq
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
Das Concept Equal
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?
Wie geht's weiter?
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.
( [5])
URL dieses Artikels:
https://www.heise.de/-4641100
Links in diesem Artikel:
[1] https://www.heise.de/blog/C-20-Vordefinierte-Concepts-4633761.html
[2] https://www.heise.de/blog/C-20-Concepts-die-Placeholder-Syntax-4607890.html
[3] https://www.heise.de/blog/C-20-Concepts-die-Placeholder-Syntax-4607890.html
[4] https://www.heise.de/blog/C-20-Zwei-Extreme-und-die-Rettung-dank-Concepts-4594834.html
[5] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien