Idiome in der Softwareentwicklung: Reguläre Datentypen

Ein regulärer Typ ist ein benutzerdefinierter Datentyp, der sich wie ein built-in Datentyp verhält.

In Pocket speichern vorlesen Druckansicht 17 Kommentare lesen

(Bild: Shutterstock.com/Kenishirotie)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

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.

  • 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.

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 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.

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:

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)