Idiome in der Softwareentwicklung: Reguläre Datentypen
Ein regulärer Typ ist ein benutzerdefinierter Datentyp, der sich wie ein built-in Datentyp verhält.
- Rainer Grimm
Das Konzept eines regulären Typs geht auf den Vater der Standard Template Library (STL) Alexander Stepanov zurück. Ein regulärer Typ ist ein benutzerdefinierter Datentyp, der sich wie ein built-in Datentyp verhält.
Der Begriff konkreter Typ (concrete type) ist eng mit dem Begriff regulärer Typ (regular type) verwandt. Deshalb möchte ich diesen Artikel mit dem Begriff des konkreten Typs beginnen.
Konkrete Typ
- Ein konkreter Typ ist laut den C++ Core Guidelines "the simplest kind of a class". Er wird oft auch als Wertetyp (value type) bezeichnet und ist nicht Teil einer Typenhierarchie.
- Ein regulärer Typ ist ein Datentyp, der "behaves like an int" und daher Kopieren und Zuweisen, Gleichheit und Ordnung unterstützen muss.
Hier sind weitere Anmerkungen aus den C++ Core Guidelines:
C.10: Prefer concrete types over class hierarchies
Der konkrete Typ ist vorzuziehen, wenn man keinen Anwendungsfall für eine Klassenhierarchie hat. Ein konkreter Typ ist viel einfacher zu implementieren, kleiner und schneller. Man muss sich nicht um Vererbung, Virtualität, Referenzen oder Zeiger kümmern, auch nicht um Speicherzuweisung und -freigabe. Es gibt kein virtuelles Dispatching und damit auch keinen Laufzeit-Overhead.
Um es kurz zu machen: Hier gilt das KISS-Prinzip (keep it simple, stupid). Der Datentyp verhält sich wie ein Wert.
C.11: Make concrete types regular
Reguläre Typen (ints) sind einfacher zu verstehen. Sie sind per se intuitiv. Das heißt, wenn man einen konkreten Typ hast, sollte man ihn zu einem regulären Typ umwandeln. Die built-in Datentypen wie int
oder double
sind regulär, aber auch die Container wie std::string, std::vector
oder std::unordered_map
.
Nun werde ich auf reguläre Typen genauer eingehen.
Regulärer Typ
Das Konzept der regulären Typen geht auf Alexander Stepanov, den Vater der STL, zurück. Er beschrieb seine Ideen über reguläre Typen in seinem Artikel "Fundamentals of Generic Programming" und verfeinerte sie in seinem Buch "Elements of Programming".
Wie profitiert man von einem regulären Datentyp? Alexander Stepanov gibt die Antwort in seinem Buch "Elements of Programming": There is a set of procedures whose inclusion in the computational basis of a type lets us place objects in data structures and use algorithms to copy objects from one data structure to another. We call types having such a basis regular, since their use guarantees regularity of behavior and, therefore, interoperability.
Um es kurz zu machen: Reguläre Typen verhalten sich in Datenstrukturen und Algorithmen intuitiv wie built-in Typen.
Wie kann ein regulärer Datentyp intuitiv sein? Laut dem Papier "Fundamentals of Generic Programming" sollte ein regulärer Datentyp die folgenden Operationen unterstützen:
Die Operationen dieser Tabelle sollten vertraut wirken. C++20 besitzt das Concept std::regular
.
Das Concept std::regular
Das Concept std::regular
ist Stepanovs Idee des regulären Datentyps sehr ähnlich. Es beinhaltet im Wesentlichen die Move-Semantik.
In C++20 wird das Konzept der regulären Typen in zwei Concepts verfeinert: std::semiregular
und std::regular
.
std:::semiregular
Ein semiregulärer Datentyp X
muss die Rule of Six unterstĂĽtzen (siehe meinen letzten Artikel "Programmiersprache C++: The Rule of Zero, or /Six") und austauschbar sein.
Ein semiregulärer Datentyp X muss die Sechserregel unterstützen (siehe meinen letzten Artikel "The Rule of Zero/Six") und vertauschbar sein:
- Default-Konstruktor:
X()
- Copy-Konstruktor:
X(const X&)
- Copy-Zuweisung:
X& operator = (const X&)
- Move-Konstruktor:
X(X&&)
- Move-Zuweisung:
X& operator = (X&&)
- Destruktor:
~X()
- Vertauschbar:
swap(X&, X&)
Es bleibt nur noch eine Eigenschaft übrig, und ein semiregulärer Datentyp X wird regulär.
- std::regular
Ein regulärer Datentyp T ist regulär und auf Gleichheit vergleichbar:
- Gleichheits-Operator:
operator == (const X&, const X&)
- Ungleichheits-Operator:
operator != (const X&, const X&)
Das Concept std::regular
basiert in C++20 auf den drei Concepts std::movable, std::copyable
und std::semiregular.
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>;
Das Concept std::movable
basiert auf der Type-Traits-Funktion std::is_object. Hier ist eine mögliche Implementierung von cppreference.com.
template< class T>
struct is_object : std::integral_constant<bool,
std::is_scalar<T>::value ||
std::is_array<T>::value ||
std::is_union<T>::value ||
std::is_class<T>::value> {};
AuĂźerdem basiert das Concept std::movable
auf den Concepts std::move_constructible
und std::assignable_from
. Alle anderen Komponenten der Concepts std::copyable
und std::semiregular
sind ebenfalls C++20 Concepts.
Mehr Information zu Concepts gibt es in meinen frĂĽheren Artikeln: Concepts:
Welcher Datentyp ist nicht regulär? Der prominenteste ist wahrscheinlich eine Referenz.
Referenzen
Eine Referenz ist kein Objekt und ist weder regulär noch semiregulär:
// referenceIsObject.cpp
#include <funktional>
#include <iostream>
#include <type_traits>
int main() {
std::cout << '\n';
std::cout << std::boolalpha;
std::cout << "std::is_object<int&>::value: "
<< std::is_object<int&>::value << '\n'; // (1)
// 2:
std::cout
<< "std::is_object<std::reference_wrapper<int>>::value: "
<< std::is_object<std::reference_wrapper<int>>::value
<< '\n';
std::cout << '\n';
}
Die Ausgabe des Programms bringt es eindeutig auf den Punkt. Eine Referenz (Zeile 1) ist kein Objekt:
Wer sich ĂĽber Zeile 2 im Programm referenceIsObject.cpp
wundert: Eine Referenz int&
ist kein Objekt, hingegen ist ein Referenz-Wrapper std::reference_wrapper<int>
ein Objekt. Hier ist die Beschreibung eines Referenz-Wrappers auf cppreference.com: std::reference_wrapper is a class template that wraps a reference in a copyable, assignable object. It is frequently used as a mechanism to store references inside standard containers (like std::vector) which cannot normally hold references.
Das bedeutet, dass ein Vektor von Referenzen std::vector<int&>
im Gegensatz zu einem Vektor von Referenz-Wrappern std::vector<std::reference_wrapper<int>>
nicht gĂĽltig ist. Daher kann man seit C++11 einen Container mit Referenzsemantik umsetzen.
// referenceSemantics.cpp
#include <functional>
#include <iostream>
#include <list>
#include <type_traits>
#include <vector>
int main() {
std::cout << '\n';
std::list<int> myList{1, 2, 3, 4, 5};
std::vector<std::reference_wrapper<int>>
myRefVector(myList.begin(), myList.end());
for (auto l: myList) std::cout << l << " ";
std::cout << "\n\n";
for (auto& v: myRefVector) v *= v; // (1)
for (auto l: myList) std::cout << l << " "; // (2)
std::cout << "\n\n";
}
Das Ă„ndern der Elemente eines std::vector<std::reference_wrapper<int>>
(1) ändert auch die referenzierten Elemente (2).
Wie verhält es sich in C++20, wenn man einen Algorithmus hat, der einen regulären Datentyp benötigt, aber eine Referenz verwendet?
// needRegularType.cpp
#include <concepts>
template <std::regular T>
class MyClass{};
int main() {
MyClass<int> myClass1;
MyClass<int&> myClass2;
}
Ich habe das Programm mit dem aktuellen GCC 12.2, dem aktuellen clang 15.0.0 und dem aktuellen msvc v19 Compiler kompiliert. Der Microsoft-Compiler lieferte die beste Fehlermeldung:
Wie geht's weiter?
Ein Value-Objekt ist ein kleines Objekt, dessen Gleichheit auf seinem Zustand, aber nicht auf seiner Identität basiert. Value-Objekte werden das Thema meines nächsten Artikels sein. (rme)