C++20: Die Concepts SemiRegular und Regular definieren

Modernes C++  –  19 Kommentare

Ein konkreter Datentyp, der sich intuitiv im C++-Ökosystem verwenden lässt, sollte sich wie ein int verhalten. Formaler ausgedrückt, sollte der konkrete Datentyp das Concept Regular unterstützen. In diesem Artikel werde ich die Concepts SemiRegular und Regular definieren.

Regular und SemiRegular sind wichtige Ideen in C++. Sorry, ich sollte Concepts sagen. Zum Beispiel lautet die Regel T.46 der C++ Core Guidelines: T.46: Require template arguments to be at least Regular or SemiRegular. Jetzt muss ich nur noch eine Frage beantworten. Was sind Regular- und SemiRegular-Datentypen? Bevor ich in die Details eintauche, ist das die informelle Antwort:

Ein Regular-Datentyp "behaves like an int". Er kann kopiert werden und das Ergebnis des Kopierens ist unabhängig vom Original. Beide haben nach dem Kopieren den gleichen Wert.

Jetzt wird es formaler. Ein Regular-Datentyp ist ein SemiRegular-Datentyp. Konsequenterweise starte ich mit einem SemiRegular-Datentyp.

SemiRegular

Ein SemiRegular-Datentyp muss die Sechserregel umsetzen und zusätzlich swap unterstützen.

  • Default-Konstruktor: X()
  • Copy-Konstruktor: X(const X&)
  • Copy-Zuweisungsoperator: operator=(const X&)
  • Move-Konstruktor: X(X&&)
  • Move-Zuweisungsoperator: operator=(X&&)
  • Destruktor: ~X()
  • swap: swap(X&, Y&)

Das war einfach. Dank der Typ-Traits-Bibliothek ist die Definition des entsprechendes Concepts reine Fingerarbeit. Erst werde ich das Typ-Trait isSemiRegular definieren und mit diesem das Concept SemiRegular umsetzen:

template<typename T>
struct isSemiRegular: 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 &&
std::is_swappable<T>::value >{};


template<typename T>
concept SemiRegular = isSemiRegular<T>::value;

Jetzt geht es weiter mit dem Concept Regular.

Regular

Ein kleiner Schritt fehlt noch, und ich kann das Concept Regular definieren. Zusätzlich zum Concept SemiRegular verlangt das Concept Regular von seinen Datentypen, dass sie auch Gleichheit unterstützen. Ich habe bereits in meinem letzten Artikel das Concept Equal implementiert:

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

Nun muss ich nur noch das Concept Equal für das Concept Regular wiederverwenden:

template<typename T>
concept Regular = Equal<T> &&
SemiRegular<T>;

Jetzt bin ich neugierig. Wie sind die beiden Concepts SemiRegular und Regular im C++20-Standard definiert?

regular und semiregular in C++20
template<class T>
concept movable = is_object_v<T> && move_constructible<T> &&
assignable_from<T&, T> && swappable<T>;

template<class T>
concept copyable = copy_constructible<T> && movable<T>
&& assignable_from<T&, const T&>;

template<class T>
concept semiregular = copyable<T> && default_constructible<T>;

template<class T>
concept regular = semiregular<T> && equality_comparable<T>;

Nebenbei gesagt, es gibt außer didaktischen Gründen keinen Grund, die Concepts Regular und SemiRegular selbst zu definieren.

Interessanterweise ist das Concept regular meinem Concept Regular sehr ähnlich. Diese Beobachtung gilt nicht für das Concept semiregular, das aus elementaren Concepts wie copyable und moveable komponiert ist. Das Concept movable basiert auf der Funktion is_object aus der Typ-Traits-Bibliothek. Auf der referenzierten Seite findet sich auch eine mögliche Implementierung des Typ-Traits is_object.

Jetzt fehlt noch der letzte Schritt in meinem Artikel: die Concepts anwenden.

Anwendung der Concepts regular und regular

Um es einfach zu machen, prüfen die Funktions-Template behavesLikeAnInt und behavesLikeAnInt2, ob die Argumente sich wie int's verhalten. Das heißt, dass ich mein Concept Regular und das Concept regular des C++20-Standards verwende, um die Typanforderung zu prüfen:

// regularSemiRegular.cpp

#include <concepts>
#include <vector>
#include <utility>

template<typename T>
struct isSemiRegular: 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 &&
std::is_swappable<T>::value >{};


template<typename T>
concept SemiRegular = isSemiRegular<T>::value;

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

template<typename T> // (1)
concept Regular = Equal<T> &&
SemiRegular<T>;

template <Regular T> // (2)
void behavesLikeAnInt(T) {
// ...
}

template <std::regular T> // (3)
void behavesLikeAnInt2(T) {
// ...
}

struct EqualityComparable { }; // (4)
bool operator == (EqualityComparable const&, EqualityComparable const&) {
return true;
}

struct NotEqualityComparable { }; // (5)

int main() {

int myInt{};
behavesLikeAnInt(myInt);
behavesLikeAnInt2(myInt);

std::vector<int> myVec{};
behavesLikeAnInt(myVec);
behavesLikeAnInt2(myVec);

EqualityComparable equComp;
behavesLikeAnInt(equComp);
behavesLikeAnInt2(equComp);

NotEqualityComparable notEquComp;
behavesLikeAnInt(notEquComp); // (6)
behavesLikeAnInt2(notEquComp); // (7)

}

Ich habe einfach alle Bausteinchen der vorherigen Codebeispiel zusammengestellt und damit das Concept Regular (Zeile 1) definiert. Die Funktionen behavesLikeAnInt (Zeile 2) und behavesLikeAnInt2 (Zeile 3) verwenden die Concepts. Wie es der Name verspricht, unterstützt der Datentyp EqualityCompareable (Zeile 4) Gleichheit. Dies gilt aber nicht für den Datentyp NotEqualityComarable (Zeile 5). Am Interessantesten ist die Anwendung des Datentyps NotEqualityComparable in den Zeilen 6 und 7.

GCC

Wenn du das Programm in Aktion sehen willst, verwende den Link auf den Compiler Explorer: https://godbolt.org/z/XAJ2w3. Die Fehlermeldung im Compiler Explorer mit dem GCC ist sehr genau, aber ein wenig unübersichtlich. Dies ist wohl der Tatsache geschuldet, dass beide Concepts fehlgeschlagen sind, Concepts sich noch in einem frühen Implementierungsstadium befinden und Online-Werkzeuge nicht so komfortabel wie eine Konsole sind.

  • Das Concept Regular

Dies sind die entscheidenden Zeilen, die mein Concept Regular (Zeile 6) mithilfe des Compiler Explorer ausgibt:

  • Das Concept regular

Das C++20-Concept regular besitzt eine ausgefeiltere Implementierung. Daher erzeugt sich auch eine ausgefeiltere Fehlermeldung:

MSVC

Die Fehlermeldung des Windows-Compilers ist mir zu unspezifisch:

Wie geht's weiter?

Mit diesem Artikel habe ich meine Miniserie zu Concepts abgeschlossen. Natürlich bin ich jetzt an deiner Meinung interessiert: Sind Concepts eine Evolution oder eine Revolution in C++? Schicke mir doch deine Meinung oder schreibe einen Kommentar, bis einschließlich Donnerstag, den 6. Februar. Gerne möchte ich das Stimmungsbild in meinem nächsten Artikel zusammenfassen. Falls ich dich namentlich nennen soll, schreibe mir dies explizit und nenne deinen Namen.