Datentypen mit Concepts prĂĽfen
Concepts sind ein mächtiges und elegantes Werkzeug, um zur Compiletime zu prüfen, ob ein Typ erfüllt ist.
- Rainer Grimm
Ich erhalte in meinen C++ Schulung oft die folgende Frage: Wie kann ich sicher sein, dass mein Datentyp verschiebbar ist? Nun, man kann entweder die Abhängigkeiten zwischen den Big Six untersuchen oder das Concept Big Six definieren und verwenden. In meinem letzten Beitrag "Datentypen mit Concepts prüfen - The Motivation" habe ich den ersten Teil der Antwort vorgestellt und die sehr komplexen Abhängigkeiten zwischen den Big Six erklärt. Zur Erinnerung: Hier sind die Big Six, einschließlich der Move-Semantik:
- Default-Konstruktor:
X()
- Copy-Konstruktor:
X(const X&)
- Copy-Zuweisungsoperator:
operator=(const X&)
- Move-Konstruktor:
X(X&&)
- Move-Zuweisungsoperator:
operator=(X&&)
- Destruktor:
~X()
Heute möchte ich das Concept BigSix definieren und verwenden.
Bevor ich das tue, habe ich einen kurzen Disclaimer: C++20 unterstĂĽtzt bereits die Concepts std::semiregular
und std::regular
.
std::semiregular
und std::regular
Ein semiregulärer Datentyp muss die Großen Sechs unterstützen und austauschbar sein:
- Default-Konstruktor:
X()
- Copy-Konstruktor:
X(const X&)
- Copy-Zuweisungsoperator:
operator=(const X&)
- Move-Konstruktor:
X(X&&)
- Move-Zuweisungsoperator:
operator=(X&&)
- Destruktor:
~X()
- Austauschbarkeit:
swap(X&, X&)
Zusätzlich verlangt std::regular
fĂĽr einen Typ X
, dass er das Concept std::semiregular
unterstützt sich auf Gleichheit prüfen lässt.
- Default-Konstruktor:
X()
- Copy-Konstruktor:
X(const X&)
- Copy-Zuweisungsoperator:
operator=(const X&)
- Move-Konstruktor:
X(X&&)
- Move-Zuweisungsoperator:
operator=(X&&)
- Destruktor:
~X()
- Austauschbarkeit:
swap(X&, X&)
- Gleichheit:
bool operator == (const X&, const X&)
Daher gibt es eigentlich keinen Grund, das Concept BigSix selbst zu definieren. Wer das Concept std::semiregular
verwendet bekommt die Eigenschaft Austauschbarkeit umsonst. Hier ist eine C++11-Implementierung von std::swap:
template <typename T>
void swap(T& a, T& b) noexcept {
T tmp(std::move(a)); // move constructor
a = std::move(b); // move assignment
b = std::move(tmp); // move assignment
}
Beim Aufruf von swap(a, b)
wendet der Compiler Move-Semantik auf die Argumente a
und b
an. Folglich unterstĂĽtzt ein Typ, der das Concept BigSix unterstĂĽtzt, auch swappable
und damit das Concept std::semiregular
.
Jetzt möchte ich das trotzdem das Concept BigSix implementieren.
Das Concept BigSix
Dank der Funktionen der Type Traits ist die Implementierung des Concept BigSix
ein Kinderspiel. Im ersten Schritt definiere ich die Type Traits isBigSix
und im zweiten Schritt verwende ich sie direkt zur Definiton des Concept BigSix
. Und los geht es:
// bigSixConcept.cpp
#include <algorithm>
#include <iostream>
#include <type_traits>
template<typename T>
struct isBigSix: std::integral_constant<bool,
std::is_default_constructible<T>::value &&
std::is_copy_constructible<T>::value &&
std::is_copy_assignable<T>::value &&
std::is_move_constructible<T>::value &&
std::is_move_assignable<T>::value &&
std::is_destructible<T>::value>{};
template<typename T>
concept BigSix = isBigSix<T>::value;
template <BigSix T> // (1)
void swap(T& a, T& b) noexcept {
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
struct MyData{ // (2)
MyData() = default;
MyData(const MyData& ) = default;
MyData& operator=(const MyData& m) = default;
};
int main(){
std::cout << '\n';
MyData a, b;
swap(a, b); // (3)
static_assert(BigSix<MyData>, "BigSix not supported"); // (4)
std::cout << '\n';
}
Die Funktion swap
setzt nun voraus, dass der Typ-Parameter T
das Concept BigSix
unterstĂĽtzt (1). In (3) rufe ich die Funktion swap
mit Argumenten vom Typ MyData
auf. AuĂźerdem prĂĽfe ich in (4) explizit, ob MyData
das Concept BigSix
unterstĂĽtzt. MyData
(2) hat einen Default-Konstruktor und unterstützt Copy-Semantik. Das Programm lässt sich kompilieren und ausführen.
Bedeutet das, dass MyData
das Concept BigSix
unterstĂĽtzt und deshalb in meine Funktion swap
verschoben wird? Ja, MyData
unterstĂĽtzt das Concept BigSix
, aber nein, MyData
wird nicht in der Funktion swap
verschoben. Die Copy-Semantik tritt als Fallback fĂĽr die Move-Semantik in Aktion.
Hier ist ein leicht verändertes Programm.
// bigSixConceptComments.cpp
#include <algorithm>
#include <iostream>
#include <type_traits>
template<typename T>
struct isBigSix: std::integral_constant<bool,
std::is_default_constructible<T>::value &&
std::is_copy_constructible<T>::value &&
std::is_copy_assignable<T>::value &&
std::is_move_constructible<T>::value &&
std::is_move_assignable<T>::value &&
std::is_destructible<T>::value>{};
template<typename T>
concept BigSix = isBigSix<T>::value;
template <BigSix T>
void swap(T& a, T& b) noexcept {
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
struct MyData{
MyData() = default;
MyData(const MyData& ) {
std::cout << "copy constructor\n";
}
MyData& operator=(const MyData& m) {
std::cout << "copy assignment operator\n";
return *this;
}
};
int main(){
std::cout << '\n';
MyData a, b;
swap(a, b);
static_assert(BigSix<MyData>, "BigSix not supported");
std::cout << '\n';
}
Ich habe Kommentare zum Copy-Konstruktor und zum Copy-Zuweisungsoperator von MyData
hinzugefĂĽgt. Wer das Programm ausfĂĽhrt, sieht, dass beide speziellen Mitgliedsfunktionen verwendet werden:
Ăśbrigens ist diese Beobachtung bereits in cppreference.com dokumentiert. So heiĂźt es zum Beispiel in einem Hinweis auf die Typ-Eigenschaft std::is_move_constructible
: "Types without a move constructor, but with a Copy-Konstruktor that accepts const T& arguments, satisfy std::is_move_constructible."
Okay, wir sind wieder am Anfang. Wir können entscheiden, ob ein Typ die BigSix
unterstützt, aber wir können nicht entscheiden, ob ein Typ wirklich verschoben wird. Wer wissen willt, ob ein Typ die Move-Semantik unterstützt und nicht, ob die Copy-Semantik als Fallback für die Move-Semantik verwendet wird, muss die Abhängigkeitstabelle in meinem vorherigen Beitrag studieren: "Datentypen mit Concepts prüfen - The Motivation".
Wie geht's weiter?
In meinem nächsten Beitrag möchte ich meine Geschichte mit Ranges in C++20 fortsetzen. Darüber hinaus erfahren Ranges in C++23 viele Verbesserungen. ()