C++20: Vordefinierte Concepts

C++20 bringt viele Concepts bereits mit. Natürlich sollte kein Concept definiert werden, das bereits existiert. Aus diesem Grund stellt der heutige Artikel die vordefinierten Concepts vor.

In Pocket speichern vorlesen Druckansicht 22 Kommentare lesen
Lesezeit: 7 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

C++20 bringt viele Concepts bereits mit. Natürlich sollte kein Concept definiert werden, das schon existiert. Aus diesem Grund stellt der heutige Artikel die vordefinierten Concepts vor. Meine bisherigen Artikel zu Concepts gibt es hier: C++20: Concepts.

Erfinde das Rad nicht neu. Diese goldene Regel sollte natürlich auch auf Concepts angewandt werden. Nebenbei gesagt, die C++ Core Guidelines bringen dies auch deutlich auf den Punkt: T.11: Whenever possible use standard concepts.

Meine Information beziehe ich aus den neuesten C++20-Entwurf: N4842. Obwohl es einen Index im Dokument zu allen Concepts gibt, ist es recht mühselig, alle Concepts zu finden. Die meisten sind im Kapitel 18 (concepts library) und im Kapitel 24 (ranges library) beschrieben. Zusätzlich gibt es weitere Concepts in den Kapiteln 17 (language support library), 20 (general utilities library), 23 (iterators library) und 26 (numeric library). Das Dokument stellt auch teilweise vor, wie Concepts implementiert sind.

Ehrlich gesagt war ich überrascht, dass ich kein Concept wie Lockable zu Concurrency gefunden habe. Ursprünglich wurden die Concepts in CamelCase oder WikiSyntax geschrieben. Nun besitzen sie Unterstriche. So wurde aus DerivedFrom etwa derived_from.

Ich ignoriere in diesem Artikel die speziellen Concepts und Hilfs-Concepts. Ich ignoriere auch die Concepts aus der Ranges-Bibliothek. Um diese zu verstehen, muss ich erst auf die Ranges-Bibliothek eingehen. Über deren Concepts werde ich dann schreiben, wenn ich sie genauer vorstelle. Diese Artikel werde direkt auf die Artikel zu den Concepts folgen.

Zuerst möchte ich aber eine Warnung aussprechen: Wenn dir technische Artikel missfallen, dann wohl auch dieser Artikel.

Dieses Kapitel besitzt das interessante Concept three_way_comparable, um den Drei-Weg-Vergleichsoperator <=> zu unterstützen.

Wenn du es formaler willst: a und b sollen vom Datentyp T sein. Sie sind genau dann three_way_comparable, wenn:

  • (a <=> b == 0) == bool(a == b) ist wahr,
  • (a <=> b != 0) == bool(a != b) ist wahr,
  • ((a <=> b) <=> 0) und (0 <=> (b <=> a)) sind gleich,
  • (a <=> b < 0) == bool(a < b) ist wahr,
  • (a <=> b > 0) == bool(a > b) ist wahr
  • (a <=> b <= 0) == bool(a <= b) ist wahr,
  • (a <=> b >= 0) == bool(a >= b) ist wahr.

Dieses Kapitel enthält die Concepts, die wir im Wesentlichen verwenden werden. Als Überschriften nutze ich im Folgenden die Überschriften des Dokuments.

Das Unterkapitel enthält gut 15 Concepts, die selbsterklärend sein sollten. Diese drücken Beziehungen zwischen Datentypen, Typklassifizierungen und fundamentalen Typeigenschaften aus. Ihre Implementierung basiert meist direkt auf der entsprechenden Funktion der Type-Traits-Bibliothek. Ich habe ihre Reihenfolge ein wenig umgestellt und füge ein paar Worte hinzu, wenn es notwendig ist.

  • same_as
  • derived_from
  • convertible_to
  • common_reference_with: common_reference_with<T, U> muss wohldefiniert und T und U zu einem Referenztyp C konvertierbar sein
  • common_with: ähnlich zu common_reference_with, aber der gemeinsame Datentyp C muss keine Referenz sein
  • assignable_from
  • swappable
  • Arithmetic
  • integral
  • signed_integral
  • unsigned_integral
  • floating_point

Die Definition der Concepts ist naheliegend:

template<class T>
concept integral = is_integral_v<T>;

template<class T>
concept signed_integral = integral<T> && is_signed_v<T>;

template<class T>
concept unsigned_integral = integral<T> && !signed_integral<T>;

template<class T>
concept floating_point = is_floating_point_v<T>;
  • Lifetime
  • destructible
  • constructible_from
  • default_constructible
  • move_constructible
  • copy_constructible
  • boolean: gibt an, ob ein Datentyp als boolean verwendet werden kann; interessanterweise gelten Zeiger, intelligente Zeiger und Datentypen mit einem expliziten Konvertierungsoperator nach bool nicht als boolean-Datentypen.
  • equality_comparable
  • totally_ordered

Für Werte a, b und c vom Datentyp T gilt, dass T das Concept totally_ordered genau dann unterstützt, wenn gilt:

  • Genau eine von bool(a < b), bool(a > b) oder bool(a == b) ist wahr.
  • Falls bool(a < b) und bool(b < c) gilt, dann bool(a < c).
  • bool(a > b) == bool(b < a).
  • bool(a <= b) == !bool(b < a).
  • bool(a >= b) == !bool(a < b).
  • movable
  • copyable
  • semiregular
  • regular

Hier ist die Definition der vier Concepts:

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

Dazu möchte ich ein paar Worte hinzufügen. Das Concept movable fordert für T, dass die Bedingung is_object_v<T> gilt. Das bedeutet im Wesentlichen, dass T ein Skalar, ein Array, eine Union oder eine Klasse sein muss.

Ich werde die Concepts semiregular und regular in zukünftigen Artikeln implementieren. Informell ausgedrückt, verhalten sich semiregular-Datentypen ähnlich wie ints, und regular-Datentypen unterstützen darüber hinaus noch den ==-Operator.

  • invocable
  • regular_invocable: F setzt das Concept invocable um und ist equality-preserving; darüber hinaus verändert F nicht die Funktionsargumente; equality-preserving bedeutet, dass F immer das gleiche Ergebnis produziert, wenn es dieselben Eingaben erhält.
  • predicate: F setzt das Concept predicate um, falls F das Concept invocable umsetzt und einen Wahrheitswert zurückgibt.

Die Concepts zu Speicher in diesem Kapitel sind zu speziell. Daher ignoriere ich sie.

Die Bibliothek besitzt viele wichtige Concepts. Hier sind zuerst einmal die Iteratorenkategorien.

  • input_iterator
  • output_iterator
  • forward_iterator
  • bidirectional_iterator
  • random_access_iterator
  • contiguous_iterator

Diese sechs Iteratorkategorien entsprechen den Concepts zu Iteratoren. Das Bild zeigt die drei wichtigsten Iteratorkategorien und die entsprechenden Container der Standard Template Library.

Die folgenden Beziehungen gelten. Ein Random Access Iterator ist ein Bidirectional Iterator und ein Bidirectional Iterator ist ein Forward Iterator. Ein Contiguous Iterator ist ein Random Access Iterator und fordert, dass seine Elemente kontinuierlich im Speicher liegen. Das heißt, dass std::array, std::vector und std::string einen Contigious Iterator unterstützen, std::deque hingegen nicht.

  • permutable: Neuordnung von Element in-place wird unterstützt
  • mergeable: Vereinigung sortierter Sequenzen in eine Ausgabesequenz wird unterstützt
  • sortable: Permutieren einer Sequenz in eine sortierte Sequenz wird unterstützt

Die numerische Bibliothek besitzt das Concept uniform_random_bit_generator. Ein uniform_random_bit_generator g vom Datentyp G muss ein unsigned int zurückgeben und jeder Wert gleich wahrscheinlich sein. Weiter fordert das Concept, dass der Generator die Funktionen G::max und G::min unterstützt.

Mit dem nächsten Artikel wird es wieder praktischer. Ich schreibe über die Definition von Concepts wie integral, semiregular und regular. Das Definieren von Concepts ist deutlich mehr, als nur Einschränkungen auf Typ-Parametern auszusprechen. ()