zurück zum Artikel

C++ Core Guidelines: reguläre und semireguläre Datentypen

Rainer Grimm

Das Thema des Blogbeitrags ist dann sehr wichtig, wenn eigene Datentypen entworfen werden: reguläre und semireguläre Datentypen.

Das Thema des Blogbeitrags ist dann sehr wichtig, wenn du eigene Datentypen entwirfst: reguläre und semireguläre Datentypen.

Genau um diese Regel geht es heute:

Die erste Frage, die ich zu beantworten habe, ist recht offensichtlich. Was ist ein regulärer oder ein semiregulärer Datentyp? Meine Antwort basiert auf dem Proposal p0898 [2]: Ich denke, du ahnst es bereits. Regular und SemiRegular sind Concepts, die auf elementareren Concepts basieren.

Der Begriff Regular geht auf Alexander Stephanov [3], den Vater der Standard Template Library zurück. Er führte ihn in seinem Buch "Fundamentals of Generic Programming" ein (zum Buch gibt es hier einen kleinen Auszug [4]). Es ist relativ einfach, die acht Concepts im Kopf zu behalten, die einen regulären Datentyp definieren. Zuerst einmal gibt es die sehr bekannte Sechserregel. [5]

Füge nun lediglich die Concepts Swappable und EqualityComparable hinzu und du erhältst das Concept Regular. Es gibt eine deutlich umgangssprachlichere Art, auszudrücken, dass ein Datentyp regulär ist: T is regulär, wenn er sich wie ein int anfühlt.

Um nun SemiRegular zu erhalten, musst du lediglich EqualityComparable von dem Concept Regular subtrahieren.

Ich höre bereits deine nächste Frage: Warum sollen unsere Template-Argumente zumindest regulär oder semiregulär sein oder sich wie ints verhalten? Die STL-Container und die Algorithmen insbesondere sind für reguläre Datentypen konzipiert.

Was ist nun ein häufig verwendeter aber nicht regulärer Datentyp? Genau, eine Referenz.

Dank der Type-Traits [6]-Bibliothek prüft das folgende Programm zu Compile-Zeit, ob int& ein semiregulärer Datentyp ist:

// semiRegular.cpp

#include <iostream>
#include <type_traits>

int main(){

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

std::cout << "std::is_default_constructible<int&>::value: "
<< std::is_default_constructible<int&>::value << std::endl;
std::cout << "std::is_copy_constructible<int&>::value: "
<< std::is_copy_constructible<int&>::value << std::endl;
std::cout << "std::is_copy_assignable<int&>::value: "
<< std::is_copy_assignable<int&>::value << std::endl;
std::cout << "std::is_move_constructible<int&>::value: "
<< std::is_move_constructible<int&>::value << std::endl;
std::cout << "std::is_move_assignable<int&>::value: "
<< std::is_move_assignable<int&>::value << std::endl;
std::cout << "std::is_destructible<int&>::value: "
<< std::is_destructible<int&>::value << std::endl;
std::cout << std::endl;
std::cout << "std::is_swappable<int&>::value: "
<< std::is_swappable<int&>::value << std::endl; // requires C++17

std::cout << std::endl;

}

Zuerst einmal setzt die Funktion std::is_swappable den C++17-Standard voraus. Hier ist die Ausgabe des Programms:

C++ Core Guidelines: reguläre und semireguläre Datentypen

Du siehst, eine Referenz wie int& besitzt keinen Default-Konstruktor. Die Ausgabe zeigt, dass eine Referenz nicht semiregulär und damit auch nicht regulär ist. Um zur Compile-Zeit zu prüfen, ob ein Datentyp regulär ist, benötige ich die Funktion isEqualityComparable. Diese Funktion ist nicht Bestandteil der Type-Traits-Bibliothek. Selbst ist der Entwickler.

In C++20 werden wir wohl das Detection-Idiom [7] erhalten, das Bestandteil des library fundamental TS v2 [8] ist. Damit ist es ein Kinderspiel, isEqualityComparable zu implementieren:

// equalityComparable.cpp

#include <experimental/type_traits> // (1)
#include <iostream>

template<typename T> // (2)
using equal_comparable_t = decltype(std::declval<T&>() == std::declval<T&>());

template<typename T>
struct isEqualityComparable:
std::experimental::is_detected<equal_comparable_t, T>{}; // (3)

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

struct NotEqualityComparable { }; // (5)

int main() {

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

std::cout << "isEqualityComparable<EqualityComparable>::value: " <<
isEqualityComparable<EqualityComparable>::value << std::endl;

std::cout << "isEqualityComparable<NotEqualityComparable>::value: " <<
isEqualityComparable<NotEqualityComparable>::value << std::endl;

std::cout << std::endl;

}

Das neue Feature gehört zum experimental Namensraum (1). Die Zeile (3) ist die entscheidende. Diese Zeile ermittelt, ob der Ausdruck (2) gültig für den Datentyp T ist. Das Type-Traits isEqualityComparable gibt die richtige Antwort für den Datentyp EqualityComparable (4) und NotEqualityComparable (5). Lediglich EqualityComparable gibt true zurück, da ich den Gleichheitsoperator überladen habe.

C++ Core Guidelines: Reguläre und semireguläre Datentypen

Bis C++20 wurden die Vergleichsoperatoren lediglich für arithmetische Datentypen, Aufzähler und mit Einschränkungen für Zeiger automatisch erzeugt. Das wird sich wohl mit C++20 dank des Spaceship-Operators [9] <=> ändern. Wenn mit C++20 eine Klasse den <=> implementiert, werden automatisch die sechs Operatoren ==, !=, <, <=, > und >= generiert. Es ist bereits ausreichend, den <=> als default zu deklarieren. Dies zeigt der Datentyp Point:

class Point {
int x;
int y;
public:
auto operator<=>(const Point&) const = default;
....
};
// compiler generates all six relational operators

In diesem Fall sorgt der Compiler für die Implementierung. Der Default-Operator <=> wendet lexikografische Vergleiche an, in dem er mit seinen Basisklassen (von links nach rechts; zuerst in die Tiefe) beginnt und dann seinen Vergleich mit den nicht-statischen Attributen in ihrer Deklarationsreihenfolge fortsetzt. Diese Vergleiche wenden Kurzschlussauswertung [10] an. Das heißt, dass die Evaluierung eines logischen Ausdrucks dann endet, wenn das Ergebnis bereits feststeht.

Jetzt sind alle Bestandteile verfügbar um Regular und SemiRegular zu definieren. Hier sind meine neuen Type-Traits:

// isRegular.cpp

#include <experimental/type_traits>
#include <iostream>

template<typename T>
using equal_comparable_t = decltype(std::declval<T&>() == std::declval<T&>());

template<typename T>
struct isEqualityComparable:
std::experimental::is_detected<equal_comparable_t, T>
{};

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>
struct isRegular: std::integral_constant<bool,
isSemiRegular<T>::value &&
isEqualityComparable<T>::value >{};


int main(){

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

std::cout << "isSemiRegular<int>::value: " << isSemiRegular<int>::value << std::endl;
std::cout << "isRegular<int>::value: " << isRegular<int>::value << std::endl;

std::cout << std::endl;

std::cout << "isSemiRegular<int&>::value: " << isSemiRegular<int&>::value << std::endl;
std::cout << "isRegular<int&>::value: " << isRegular<int&>::value << std::endl;

std::cout << std::endl;

}

Durch die Verwendung der neuen Type-Traits isSemiRegular und isRegular ist das Programm deutlich einfacher zu lesen.

C++ Core Guidelines: Reguläre und semireguläre Datentypen

Mit meinem nächsten Blogbeitrag springe ich direkt in die Definition von Templates. ( [11])


URL dieses Artikels:
https://www.heise.de/-4232030

Links in diesem Artikel:
[1] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-regular
[2] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0898r3.pdf
[3] https://en.wikipedia.org/wiki/Alexander_Stepanov
[4] http://stepanovpapers.com/DeSt98.pdf
[5] https://www.heise.de/developer/artikel/C-Core-Guidelines-Die-Nuller-Fuenfer-oder-Sechserregel-3813435.html
[6] https://en.cppreference.com/w/cpp/header/type_traits
[7] https://en.cppreference.com/w/cpp/experimental/is_detected
[8] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4562.html
[9] https://en.cppreference.com/w/cpp/language/default_comparisons
[10] https://de.wikipedia.org/wiki/Kurzschlussauswertung
[11] mailto:rainer@grimm-jaud.de