13 Metaprogrammierung mit Templates |
Dieses Kapitel behandelt die folgenden Themen:
Template-Metaprogrammierung
Variadische Funktions-Templates
Fold-Expressions
Variadische Klassen-Templates
Type Traits
Concepts
Das Schreiben von Programmcode, der selbst Programme oder Programmteile erzeugt, nennt man Metaprogrammierung. Metaprogrammierung ist ein in mehreren Fachgebieten und Programmiersprachen vorkommender Begriff. Hier geht es ausschließlich um Metaprogrammierung mit C++-Templates. Wenn Sie ein Template für eine Klasse oder eine Funktion schreiben, sehen Sie einen Platzhalter vor. Erst der Compiler erzeugt bei der Instanziierung des Templates eine Klasse oder eine Funktion mit einem konkreten Datentyp. Wie bei constexpr ist die Auswertung zur Compilationszeit wesentliches Merkmal: Alles, was zu dieser Zeit bereits erledigt werden kann, benötigt später keine Laufzeit und trägt zur Geschwindigkeit des entstehenden Programms bei. Bei Templates kann der Platzhalter durch einen konkreten Datentyp ersetzt werden. Es sind aber auch Platzhalter möglich, die keine Typen sind, sondern Werte. Dies zeigt das Stack-Template mit einstellbarer Größe aus Abschnitt 5.5.2. Diese Werte gehen in den aus dem Template erzeugten Datentyp ein. Die zum großen Teil darauf basierende Template-Metaprogrammierung wird in großem Umfang in der C++-Standardbibliothek eingesetzt.
13.1 | Grundlagen |
Zur Vertiefung des Verständnisses von Templates wird zunächst gezeigt, wie der Compiler zum Rechnen gebracht werden kann. In C++ geht es bei der Template-Metaprogrammierung jedoch meistens nicht um arithmetisches Rechnen, sondern darum, wie der Programmcode schon zur Compilationszeit optimiert werden kann, etwa indem der beste Algorithmus für einen Datentyp bestimmt wird.
Das folgende Programm berechnet eine Zweierpotenz, im Beispiel 211 = 2048. Der Compiler versucht bei der Übersetzung, den Wert des Attributs Zweihoch<11>::Wert zu ermitteln. Die klassenspezifische Aufzählungskonstante Wert hat für jeden Typ der Klasse Zweihoch, der von n abhängt, einen anderen Wert. Bei der Ermittlung stellt der Compiler fest, dass Zweihoch<11>::wert dasselbe wie 2·Zweihoch<10>::wert ist. Zweihoch<10>::wert wiederum ist dasselbe wie 2·Zweihoch<9>::wert usw. Natürlich ist dasselbe Ergebnis mit einer constexpr-Funktion zu erreichen, aber hier geht es darum, dass mithilfe von Typen gerechnet werden kann.
#include <iostream> template <int n> struct Zweihoch { static const int wert{2 * Zweihoch<n - 1>::wert}; }; template <> struct Zweihoch<0> { static const int wert{1}; }; int main() { std::cout << Zweihoch<11>::wert << ’\n’; }
Die Rekursion bricht bei der Berechnung von Zweihoch<0>::wert ab, weil das Template für diesen Fall spezialisiert und der Wert mit 1 besetzt ist. Der Compiler erzeugt insgesamt zwölf Datentypen (0 bis 11), die er zur Auswertung heranzieht. Weil er konstante Ausdrücke zur Compilationszeit kennt und berechnen kann, wird an die Stelle von Zweihoch<11>::wert direkt das Ergebnis 2048 eingetragen, sodass zur Laufzeit des Programms keinerlei Rechnungen mehr nötig sind!
Diese Methode zur Berechnung von Zweierpotenzen schlägt damit jede andere, was die Rechenzeit des Programms angeht. Dieses Verfahren wird mit gutem Erfolg erweitert auf andere Probleme wie zum Beispiel die Berechnung der schnellen Fouriertransformation und die Optimierung von Vektoroperationen [Ve], stellt aber hohe Anforderungen an die verwendeten Compiler. Insbesondere ist die Tiefe der möglichen Template-Instanziierungen begrenzt.
Als Ergänzung werden im folgenden Beispiel Primzahlen vom Compiler berechnet, aber erst zur Laufzeit ausgegeben. Dabei gibt es keinerlei Schleifen oder Funktionsaufrufe, nur die statische, allerdings rekursive Konstruktion von Objekten. Die Spezialisierungen sorgen für den Abbruch der Rekursion. Anstelle der 17 kann eine andere Zahl stehen. Die mögliche Höchstzahl ist abhängig vom verwendeten Compiler. Dieses Programm wurde nach einer Idee von Erwin Unruh [Unr] geschrieben, der 1994 ein Programm konstruierte, das bei Übersetzung Primzahlen in den Fehlermeldungen des Compilers erzeugte. Für sich genommen, scheinen beide Beispiele eher Kuriositäten zu sein. Als Übung zum Verständnis des folgenden Abschnitts sind sie aber gut geeignet. Außerdem werden ähnliche Programmiertechniken von der C++-Standardbibliothek nicht nur verwendet, sondern auch stark unterstützt.
#include <iostream> template <int p, int i> struct istPrimzahl { // p ist nur dann prim, wenn p nicht durch i teilbar ist und auch nicht durch alle // anderen Teiler zwischen 2 und i. Wenn i==2 ist, wird istPrimzahl<0, 1>::prim // gefragt, d.h. Abruch der Rekursion (s.u.). static const int prim{(p % i) && istPrimzahl<(i > 2 ? p : 0), i - 1>::prim}; }; // Rekursionsabbruch durch Spezialisierung template <> struct istPrimzahl<0, 1> { static const int prim = 1; }; template <int i> struct druckePrimzahlenBis { // Der folgende Konstruktoraufruf sorgt dafür, dass auch die kleineren Primzahlen // rekursiv ausgegeben werden. druckePrimzahlenBis<i - 1> a; static const int primzahl{istPrimzahl<i, i - 1>::prim}; druckePrimzahlenBis() { if (primzahl) { std::cout << i << ’\n’; } } }; // Rekursionsabbruch durch Spezialisierung template <> struct druckePrimzahlenBis<2> { druckePrimzahlenBis() { std::cout << 2 << ’\n’; } }; int main() { druckePrimzahlenBis<17> a; }
Die in diesem Abschnitt diskutierte Metaprogrammierung bekommt steigendes Gewicht. Die Boost- und die C++-Standardbibliothek basieren großenteils darauf. Es geht darum, schon zur Compilationszeit abhängig vom Typ oder bestimmten Eigenschaften Entscheidungen zu treffen, etwa welcher Algorithmus gewählt werden soll. Diese Typinformationen sind in sogenannten Traits-Klassen festgelegt. Traits heißt Eigenschaft oder Merkmal. Der C++-Standard unterstützt Metaprogrammierung durch Bereitstellung von Templates für Typinformationen. Mehr dazu finden Sie unten in Abschnitt 13.5.
Das obige Beispiel zeigt, dass nicht nur mit constexpr, sondern auch mit Typen schon zur Compilationszeit gerechnet werden kann. Schleifen werden beim Rechnen mit Typen stets mit einer Rekursion realisiert. Menschen mit Lisp- oder Prolog-Erfahrung wird das bekannt vorkommen.
Übungen
13.1 Schreiben Sie mithilfe der Template-Metaprogrammierung eine Template-Struktur Fakultaet, mit der die Fakultät n! = 1·2·3 . . . n einer int-Zahl n ≥ 0 zur Compilationszeit berechnet werden kann. 0! ist dabei als 1 definiert. Das main()-Programm soll 8!, d.h. 40320, ausgeben, etwa wie hier gezeigt:
13.2 Schreiben Sie mithilfe der Template-Metaprogrammierung eine Template-Klasse namens Fibonacci, mit der Fibonacci-Zahlen zur Compilationszeit berechnet werden können. Eine Fibonacci-Zahl f ist die Summe ihrer beiden Vorgänger, also fn = fn–1 + fn–2. Dabei seien f0 = 0 und f1 = 1. Das main()-Programm soll zum Beispiel die elfte Fibonacci-Zahl ausgeben.
13.3 Schreiben Sie die Fibonacci-Funktion als constexpr-Funktion auf zwei Arten: a) rekursiv, b) iterativ.
13.4 Welche der zwei Lösungen der vorherigen Aufgabe ist effizienter und warum?
13.2 | Variadic Templates: Templates mit variabler Parameterzahl |
Die Anzahl der Parameter von Funktionen und Operatoren wird Stelligkeit oder Arität genannt. Die Addition ist zweistellig, weil sie zwei Argumente benötigt. Bisher werden Templates in diesem Buch mit einer festen Anzahl von Parametern (Typen) definiert. Bei der Benutzung müssen die Typen entsprechend der Anzahl angegeben werden. Die Stelligkeit ist festgelegt. Das Stack-Template von Seite 296 hat die Stelligkeit zwei:
Die C++-Standardbibliothek kann in vielen Teilen einfacher geschrieben werden, seitdem es Templates mit variabler Stelligkeit (englisch variadic templates) gibt. Ein Beispiel dafür ist die Bibliotheksklasse tuple (für Tupel) von Seite 837. Ein anderes Beispiel ist eine Funktion zum Ausdrucken aller Parameter, bei der unbekannt ist, mit wie vielen Argumenten sie aufgerufen werden wird. Für jede beliebige Anzahl von Parametern jeweils eine Funktion zu schreiben, ist nicht praktikabel. Aus diesem Grund gibt es in der Programmiersprache C die Funktion int printf(const char* format, ...), der eine beliebig lange Parameterliste übergeben werden kann. format ist der C-String, der die Formatierung steuert. Die drei Punkte heißen Ellipse. Ellipse bedeutet Auslassung. Die Punkte stehen für eine Folge von Parametern. So gibt der Aufruf printf("Wert = %.4f\n", 1.2345678); die Zeile »Wert = 1.2346« aus. Dabei meint %.4f\n, dass eine Float-Zahl mit vier Dezimalstellen nach dem Komma ausgegeben und dann eine neue Zeile begonnen wird. printf() gibt die Anzahl der ausgegebenen Zeichen zurück bzw. -1 bei einem Fehler. Die Funktion printf() ist jedoch nicht typsicher, das heißt, falsche Typen in der Parameterliste können nicht schon vom Compiler entdeckt werden.
Douglas Gregor hatte unter der Überschrift »Variadic Templates« einen Vorschlag, wie Ellipsen typsicher gestaltet werden können, entwickelt. Der Vorschlag wurde in den C++-Standard aufgenommen. Einen Übersichtsartikel finden Sie unter [GrJ].
13.2.1 | Ablauf der Auswertung durch den Compiler |
Wie Templates mit variabler Stelligkeit funktionieren, sei am Beispiel einer Funktion anzeigen() demonstriert, der beliebig viele Parameter zur Ausgabe mit cout << übergeben werden können. Der Compiler löst die beliebig vielen Parameter rekursiv auf – angewandte Template-Metaprogrammierung! Dabei prüft er, ob der Typ eines Parameters überhaupt zum Ausgabeoperator << passt. Die Rekursion kann mithilfe einer anderen Syntax vermieden werden – wie, ist Thema des Abschnitts 13.3.
#include <iostream> void anzeigen() { std::cout << ’\n’; } template <typename T, typename... Rest> void anzeigen(const T& obj, const Rest&... rest) { std::cout << obj << ’˽’; anzeigen(rest...); } int main() { anzeigen(1); anzeigen(2, "Hallo"); anzeigen("Text", 7.978353, 3); }
Wie Sie sehen, wird anzeigen() mit einem bis drei Parametern aufgerufen. Das entscheidende Element ist die Template-Definition:
Die Ellipse ... nach typename bedeutet, dass an dieser Stelle null oder mehr Template-Parameter in Rest zusammengefasst werden. Der Parameter Rest wird »Template Parameter Pack« genannt. Wenn anzeigen() mit mehreren Parametern aufgerufen wird, wird der erste T zugeordnet, alle anderen dem Parameter Rest – daher der Name.
In der Parameterliste der Funktion wird Rest fast wie ein normaler Template-Parameter verwendet. Der syntaktische Unterschied besteht nur darin, dass eine Ellipse ... folgt, um die Eigenschaft »Template Parameter Pack« zu markieren.
Dasselbe gilt für den Funktionsaufruf anzeigen(rest...): Im Aufruf ist rest..., gekennzeichnet durch eine Ellipse, ein »Template Parameter Pack«. Dieser Aufruf hat einen Parameter weniger als die aufrufende Funktion!
Wie verarbeitet der Compiler die Anweisung anzeigen("Text", 7.978353, 3);? Die Abfolge in einzelnen Schritten ist:
Zuerst wird eine Ausgabe für das erste Objekt, den C-String "Text", erzeugt. Der anschließende Aufruf anzeigen(rest...); ist nichts anderes als anzeigen(7.978353, 3); – es wird nur der Rest übergeben.
Der Aufruf anzeigen(7.978353, 3); wird genauso behandelt; es wird 7.978353 ausgegeben und dann anzeigen(3); aufgerufen.
anzeigen(3); resultiert in der Ausgabe von 3 und dem Aufruf anzeigen();.
anzeigen(); ruft die Funktion ohne Parameter oben in der Datei auf. Diese Funktion beendet nur die laufende Zeile.
Der Compiler erzeugt aus dem Aufruf anzeigen("Text", 7.978353, 3); also die folgenden Funktionen aus dem Template:
Der Typ const char * const& bedeutet const-Referenz auf const char*. Die Funktion ohne Parameter (anzeigen()) ist schon vorhanden und wird daher nicht erzeugt. Sie beendet die Rekursion.
13.2.2 | Anzahl der Parameter |
Die Anzahl der Parameter kann mit sizeof...(parameterPack) ermittelt werden. Der sizeof...-Operator gibt die Anzahl der Typen bzw. der Argumente des Parameter-Packs zurück. Die obige Funktion anzeigen() könnte zur Demonstration erweitert werden:
template<typename T, typename... Rest> void anzeigen(const T& obj, const Rest&... rest) { std::cout << obj << ’˽’; std::cout << "˽[" << sizeof...(Rest) << "˽Typ-Parameter]˽"; std::cout << "˽[" << sizeof...(rest) << "˽Funktions-Parameter]˽"; anzeigen(rest...); }
13.2.3 | Parameterexpansion |
Es gibt mehrere Arten der Expansion, von denen hier drei genannt werden. Das Parameter-Pack heiße args. Es bestehe beispielsweise aus drei Parametern p1, p2 und p3.
f(args...) wird zu f(p1, p2, p3)
Diese Art der Expansion haben Sie oben gesehen. Der Aufruf anzeigen(rest...) ist also gleichbedeutend mit anzeigen(p1, p2, p3).
f(++args...) wird zu f(++p1, ++p2, ++p3)
Das folgende Beispiel zeigt die Wirkung dieser Art der Expansion im Vergleich zur vorherigen. Anstelle des ++-Operators kann ein anderer stehen.
#include <iostream> void anzeigen() { std::cout << ’\n’; } template <typename T, typename... Rest> void anzeigen(const T& obj, const Rest&... rest) { std::cout << obj << ’˽’; anzeigen(rest...); } template <typename... Args> void f1(const Args&... args) { anzeigen(args...); // Expansion p1, p2, p3 } template <typename... Args> void f2(Args... args) // Kopie, weil args geändert wird { anzeigen(++args...); // Expansion ++p1, ++p2, ++p3 } int main() { f1(1, 2, 3); // 1 2 3 f2(1, 2, 3); // 2 3 4 }
Im Programm gibt es eine überladene Funktion ohne Parameter, die die rekursive Auswertung der Aufrufe durch den Compiler beendet. Hier ist sie zusätzlich hilfreich, weil sie die Anzeige mit \n beendet. Im Listing 13.6 sehen Sie, wie mit if constexpr die für den Rekursionsabbruch benötigte Funktion eingespart werden kann.
f(&args...) wird zu f(&p1, &p2, &p3)
Das Programm nimmt die Adressen der Parameter. Das erlaubt es, die Werte der Parameter zu ändern. Hier werden sie verdoppelt.
#include <iostream> template <typename T, typename... Rest> void verdoppeln(const T& obj, const Rest&... rest) { *obj *= 2; if constexpr (sizeof...(rest) > 0) { verdoppeln(rest...); } } template <typename... Args> void f3(Args&... args) { verdoppeln(&args...); // Expansion &p1, &p2, &p3 } int main() { int i1{1}; int i2{7}; f3(i1, i2); std::cout << i1 << ’˽’ << i2 << ’\n’; // 2 14 }
if constexpr wertet nur eine Bedingung aus, die schon zur Compilationszeit bekannt ist. Hier bewirkt sie, dass es keinen Aufruf der Funktion verdoppeln() mit leerer Parameterliste geben kann. Damit entfällt die Notwendigkeit, eben diese Funktion zu definieren.
13.3 | Fold-Expressions |
Fold-Expressions (dt. etwa Faltungsausdrücke) sind Vereinfachungen für eine Folge von Parametern. Das Wort »fold« stammt aus der funktionalen Programmierung und bezieht sich auf eine Familie von Funktionen, die eine rekursive Datenstruktur analysieren. Zunächst sei ein Programm vorgestellt, das eine im Vergleich zu oben geänderte Funktion anzeigen() enthält. Sie kann im Prinzip beliebig viele Parameter haben. Im Aufruf sind es drei. Sie sehen sofort, dass dieses Programm ohne Rekursion auskommt und daher einfacher im Vergleich zum obigen ist.
#include <iostream> #include <string_view> template <typename... Args> void anzeigen(const Args&... args) { (std::cout << ... << args) << ’\n’; // Fold-Expression } int main() { std::string_view einString("String˽"); anzeigen(einString, "Textliteral˽", 7.978353); }
Das Entscheidende ist die mit »Fold-Expression« markierte Zeile. Sie hat den Aufbau
Dabei ist E ein Ausdruck, der kein zu expandierendes Parameter-Pack enthält (std::cout in der Zeile). ⊗ steht für einen binären Operator, oben ist es der Ausgabeoperator <<. Der binäre Operator kann in anderen Fällen nahezu jeder andere binäre Operator sein, wie etwa +, *, ==, += usw. Auch der Kommaoperator , gehört dazu. P steht für einen Ausdruck, der das zu expandierende Parameter-Pack enthält. Die runden Klammern müssen sein! Wenn genau drei Parameter vorhanden sind, wird so eine Zeile vom Compiler wie folgt expandiert:
wobei p1, p2 usw. die einzelnen Teile des Parameter-Packs sind, auf die gegebenenfalls eine in P beschriebene Operation angewendet wird. Anstelle der Funktion kann auch ein unärer Operator wie zum Beispiel ++ verwendet werden, wie in Abschnitt 13.2.3 bei der Parameterexpansion gesehen. Ein Fold-Expression
Hinweis
Im Folgenden wird zur Vereinfachung der Beschreibung ein einfaches Parameter-Pack ohne Funktion oder Operator angenommen.
Entsprechendes gilt für mehr oder weniger Parameter. Bei vier Parametern ergäbe sich
Die Fold-Expression-Zeile des obigen Programms wird daher zu
ausgewertet.
13.3.1 | Weitere Varianten |
Um nicht eine unbestimmte Anzahl von Klammern abbilden zu müssen, wird hier die allgemeine Expansion der Fold-Expressions mit vier Parametern beschrieben. Im Einzelfall ist natürlich die tatsächliche Anzahl der Parameter maßgebend.
(P ⊗ ... ⊗ E) wird zu p1 ⊗ (p2 ⊗ (p3 ⊗ (p4 ⊗ E)))
Im Vergleich zu oben sind E und P vertauscht und die Auswertung geschieht von rechts statt von links. Ein Beispiel:
#include <iostream> template <typename T, typename... Args> int addiere_auf_anfangswert(const T& anfangswert, const Args&... args) { return (args + ... + anfangswert); } int main() { std::cout << addiere_auf_anfangswert(10, 1, 2, 3, 4) << ’\n’; // 20 }
Statt anfangswert darf kein Ausdruck stehen, der eine höhere Priorität enthält, wie etwa 3*anfangswert (Punktrechnung geht vor Strichrechnung). Wenn dieser Ausdruck geklammert wird, also (3*anfangswert), gibt es kein Problem.
(P ⊗ ...) wird zu p1 ⊗ (p2 ⊗ (p3 ⊗ p4))
Wenn kein Anfangswert gebraucht wird, vereinfacht sich das vorherige Beispiel.
#include <iostream> template <typename... Args> int summe(const Args&... args) { return (args + ...); } int main() { std::cout << summe(1, 2, 3, 4, 5) << ’\n’; }
(... ⊗ P) wird zu ((p1 ⊗ p2) ⊗ p3) ⊗ p4
Bei einer Summe ist es egal, ob von rechts oder von links aufsummiert wird. Deswegen lassen sich hier P und ... vertauschen. Bei anderen Problemstellungen ist das nicht unbedingt möglich.
#include <iostream> template <typename... Args> int summe(const Args&... args) { return (... + args); } int main() { std::cout << summe(1, 2, 3, 4, 5) << ’\n’; }
13.3.2 | Fold-Expression mit Kommaoperator |
Der Kommaoperator in einem Fold-Expression bewirkt, dass eine Operation auf jeden Parameter angewendet wird. Das heißt, (f(args) , ...) wird zu f(p1), f(p2), f(p3) usw. Damit lässt sich das Listing 13.6 von Seite 516 einfacher schreiben:
#include <iostream> void verdoppeln(int& x) { x *= 2; } template <typename... Args> void f4(Args&... args) { (verdoppeln(args), ...); } int main() { int i1{1}; int i2{7}; f4(i1, i2); std::cout << i1 << ’˽’ << i2 << ’\n’; // 2 14 }
Man kann ohne die Funktion verdoppeln() auskommen, wenn die Operation »Multiplikation mit 2« direkt in die Funktion integriert wird:
#include <iostream> template <typename... Args> void f5(Args&... args) { ((args *= 2), ...); // Angabe der Operation in Klammern } int main() { int i1{1}; int i2{7}; f5(i1, i2); std::cout << i1 << ’˽’ << i2 << ’\n’; // 2 14 }
Dabei sind die Klammern zwingend, weil der Kommaoperator die niedrigste Priorität hat. Eine weitere Alternative ist die Übergabe einer Lambda-Funktion:
#include <iostream> template <typename... Args, typename Func> void f6(Func f, Args&... args) { (f(args), ...); } int main() { int i1{1}; int i2{7}; f6( [](auto& el) { el *= 2; }, // Lambda-Funktion i1, i2); std::cout << i1 << ’˽’ << i2 << ’\n’; // 2 14 }
13.4 | Klassen-Template mit variabler Stelligkeit |
Templates mit variabler Stelligkeit sind nicht nur für Funktionen, sondern auch für Klassen möglich. Das wohl bekannteste Klassen-Template mit variabler Stelligkeit ist die Klasse tuple der Standardbibliothek (siehe Seite 837). Listing 13.14 zeigt eine vergleichsweise einfache Struktur. Die Auswertung geschieht zur Compilationszeit:
#include <iostream> template <typename... Args> struct Anzahl { static auto anzahlDerTypen() { return sizeof...(Args); } }; int main() { std::cout << "Anzahl˽der˽Typen˽von˽Anzahl<char*,˽int,˽double>:˽" << Anzahl<char*, int, double>::anzahlDerTypen() << ’\n’; }
13.5 | Type Traits |
C++ unterstützt die Template-Metaprogrammierung, weil sich mit ihr viele Aufgaben schon zur Compilationszeit erledigen lassen. Unter anderem ist es möglich, aus mehreren Algorithmen den effizientesten zu wählen, wie unten gezeigt wird. Zur Unterstützung der C++-Standardbibliothek und auch selbstgeschriebener Programme bietet C++ sogenannte Type Traits an [ISOC++, meta]. Das sind öffentliche Klassen (struct), die bestimmte Typinformationen liefern oder mit deren Hilfe Typen umgewandelt werden können. Sie sind im Header <type_traits> definiert. Type-Traits können vorteilhaft in Concepts verwendet werden, das Thema des nächsten Abschnitts 13.6.
Hinweis
Ziel ist, die Wirkungsweise der wichtigsten Type Traits kennenzulernen. Auf dieser Basis können dann Quellen wie [ISOC++] ergänzend hinzugezogen werden. Deshalb wird eine Auswahl der Type Traits dargestellt, oft mit beispielhaften Anwendungen.
So gibt etwa is_class an, ob ein Typ eine Klasse ist. Das Programm in Listing 13.15 gibt false, true, true aus, denn int ist kein Klassentyp, X und vector<double> sind es jedoch schon.
#include <iostream> #include <type_traits> // is_class #include <vector> class X {}; int main() { std::cout << std::boolalpha; std::cout << std::is_class<int>::value << ’\n’; std::cout << std::is_class<X>::value << ’\n’; std::cout << std::is_class<std::vector<double>>::value << ’\n’; }
13.5.1 | Wie funktionieren Type Traits? — ein Beispiel |
Um zu zeigen, wie Type Traits funktionieren, wird unten eine Klasse has_sort_function< Container> definiert, die erkennt, ob ein Container eine Funktion sort() hat. Ein vector-Objekt kann wegen des wahlfreien Zugriffs mit dem allgemeinen Sortieralgorithmus std::sort() sortiert werden. Ein list-Objekt ist nicht damit sortierbar, weswegen die Klasse list eine spezialisierte Funktion zum Sortieren enthält. Ein Algorithmus, der entscheiden soll, ob eine containerspezifische sort()-Funktion oder std::sort() aufzurufen ist, benötigt ein Entscheidungskriterium. Dieses bietet has_sort_function<Container>. Für die Realisierung werden die Typen true_type und false_type benutzt, die wie folgt definiert sind:
// Zitat aus [ISOC++, meta.help] template <class T, T v> struct integral_constant { static constexpr T value = v; using value_type = T; using type = integral_constant<T, v>; constexpr operator value_type() const noexcept { return value; } constexpr value_type operator()() const noexcept { return value; } };
Von diesem Klassentemplate werden die Typnamen std::true_type und std::false_type abgeleitet:
integral_constant und sein Alias bool_constant werden für die Definition verschiedener Type Traits genutzt. v ist kein Typ, sondern ein Wertparameter des Typs T. Solche Parameter müssen von einem integralen Typ (bool, char, int usw.) oder ein Zeiger sein, andernfalls ist das Template nicht übersetzbar. Damit hat true_type::value den Wert true und false_type::value den Wert false. Das Template has_sort_function<T> erbt von false_type:
Der zweite Template-Parameter hat keinen Namen, weil er nicht benutzt wird. Sein Typ ist void, weil das auch der Rückgabetyp der Funktion sort() ist.
Die folgende Spezialisierung ersetzt den zweiten Template-Parameter durch den Typ der sort()-Funktion des Containers – die dazu aber vorhanden sein muss! Der Typ ist natürlich auch void, aber er wird aus der Funktion T::sort() abgeleitet:
Bei dieser Formulierung müssen keine Kenntnisse über den Konstruktor von T vorliegen. Wenn bekannt ist, dass zum Beispiel der Standardkonstruktor T() existiert, kann man kürzer schreiben:
Natürlich könnte man ohne true_type bzw. false_type auskommen, wenn ein entsprechender Wahrheitswert value direkt in den Klassen definiert würde. In den Implementierungen der Type Traits der C++-Standardbibliothek werden true_type bzw. false_type jedoch häufig verwendet, weswegen hier ebenso vorgegangen wird. Wenn der Compiler versucht, das Template zu instanziieren, gibt es zwei Fälle:
1. Der Typ T hat keine sort()-Funktion.
Die Instanziierung des zweiten Templates scheitert, sodass das erste Template zur Geltung kommt. Weil es von std::false_type erbt, ist has_sort_function<T>::value == false. Das Attribut value wird von std::false_type geerbt (siehe oben).
2. Der Typ T besitzt eine sort()-Funktion.
In diesem Fall könnten beide Templates instanziiert werden, weil der zweite Template-Parameter bei Template 1 und Template 2 den Typ void ergibt. Das zweite Template ist aber das spezialisierte und wird deswegen ausgewählt. Weil es von std::true_type erbt, gilt has_sort_function<T>::value == true.
Dass im ersten Fall die Substitution der Template-Argumente fehlschlägt, ist also kein Fehler, solange es ein anderes passendes Template gibt. Der englische Ausdruck für diese Technik ist substitution failure is not an error, oft mit SFINAE abgekürzt.
Zur Compilationszeit kann somit entschieden werden, ob ein Typ eine sort()-Funktion hat. In dem Listing 13.17 wird das ausgenutzt, um zur Compilationszeit den richtigen Sortieralgorithmus auszuwählen.
#include <algorithm> #include <iostream> #include <list> #include <showContainer.h> // siehe Text #include <type_traits> #include <utility> // declval #include <vector> template <typename T, typename = void> // void = Rückgabetyp von sort() struct has_sort_function : std::false_type {}; template <typename T> struct has_sort_function<T, decltype(std::declval<T>().sort())> : std::true_type {}; template <typename Container> void sortiereContainer(Container& container) { if constexpr (has_sort_function<Container>()) { std::cout << "Container.sort()\n"; container.sort(); } else { std::cout << "std::sort()\n"; std::sort(container.begin(), container.end()); } } int main() { std::cout << std::boolalpha; std::cout << has_sort_function<std::list<int>>::value << ’\n’; std::cout << has_sort_function<std::vector<int>>::value << ’\n’; std::list<int> liste{3, 6, 8, 9, 1, 3, 0}; std::vector<int> vektor{3, 6, 8, 9, 1, 3, 0}; sortiereContainer(liste); showContainer(liste); sortiereContainer(vektor); showContainer(vektor); }
Die Hilfsfunktionen showContainer() zeigen einen Container oder einen Range auf dem Bildschirm an. Die Header-Datei liegt im Verzeichnis cppbuch/include der Beispiele und ist wie folgt definiert:
#ifndef SHOWCONTAINER_H #define SHOWCONTAINER_H #include <iostream> #include <string_view> template <class Container> void showContainer(Container&& container, std::string_view abschluss = "\n", std::string_view trennzeichen = "˽") { for (decltype(auto) element : container) { std::cout << element << trennzeichen; } std::cout << abschluss; } template <class Iterator> void showContainer(Iterator i1, Iterator i2, std::string_view abschluss = "\n", std::string_view trennzeichen = "˽") { while (i1 != i2) { std::cout << *i1++ << trennzeichen; } std::cout << abschluss; } #endif
Der Typ Container&& spart gegebenenfalls die Kopie eines temporären Containers ein. decltype(auto) ermittelt den Typ und erhält dabei die Referenz. Nur auto& zu schreiben passt nicht für jede Art von Container, zum Beispiel vector<bool>, weil die Adresse eines Bits nicht genommen werden kann. Die zweite Funktion ist für den Fall, dass nur ein Teil des Containers zur Anzeige gebracht werden soll. Die Iteratoren kennzeichnen Anfang und Ende des anzuzeigenden Bereichs. Beim Aufruf muss sichergestellt sein, dass i2 mit der ++-Operation von i1 aus erreichbar ist. Eine einfache Prüfung der Art assert(i1 <= i2) funktioniert bei einem Vektor, genauer: bei Random-Access-Iteratoren, nicht aber, wenn der Container zum Beispiel ein set oder eine Liste ist.
13.5.2 | Abfrage von Eigenschaften |
Es gibt viele Type Traits zur Abfrage von Eigenschaften eines Typs. Bevor sie genannt werden, wird zunächst am Beispiel von is_arithmetic gezeigt, wie ein Type Trait zur Abfrage einer Typeigenschaft konkret angewendet wird. Im folgenden Programm wird die Summe aller Elemente eines Containers berechnet. Dabei können die Container verschiedenen Typs sein, etwa list oder vector, ebenso wie die Elemente, etwa int oder double. Die Summe kann sinnvoll nur aus Zahlen berechnet werden. Mit is_arithmetic kann eine mathematische Funktion sicherstellen, dass der Aufrufer den richtigen Datentyp verwendet. Dabei wird ausgenutzt, dass die Container der Standardbibliothek den Datentyp der Elemente mit Container::value_type zur Verfügung stellen:
#include <iostream> #include <list> #include <type_traits> #include <vector> template <typename Container> typename Container::value_type summe(const Container& cont) { static_assert(std::is_arithmetic<typename Container::value_type>::value, "Die˽Elemente˽des˽Containers˽muessen˽Zahlen˽sein!"); typename Container::value_type ergebnis{}; for (auto wert : cont) { ergebnis += wert; } return ergebnis; } int main() { const std::vector<int> vektor{3, 6, 8, 9, 1, 3, 0}; std::cout << "Summe˽vector<int>˽˽=˽" << summe(vektor) << ’\n’; const std::list<double> liste{3, 6, 8, 9, 1, 3, 0}; std::cout << "Summe˽list<double>˽=˽" << summe(liste) << ’\n’; }
Die folgende Ergänzung des Programms ist nicht übersetzbar, weil mit Strings nicht gerechnet werden kann:
std::is_arithmetic<typename Container::value_type>::value ist nur dann true, wenn mit den Containerelementen gerechnet werden kann. Ohne die static_assert-Anweisung würde die Funktion übersetzt werden und im letzten Fall ein falsches Ergebnis liefern, nämlich den String »abcdef«. Die folgenden Templates werden auf ähnliche Weise wie is_arithmetic benutzt. Das heißt, es wird die boolesche Konstante value ausgewertet, etwa std::is_integral<int>::value. Diese Type Traits erben die Konstante value von der oben erwähnten Hilfsklasse integral_constant, also von true_type bzw. false_type.
Abkürzende Schreibweise mit _v
Für alle C++-Traits gilt, dass ::value nicht geschrieben werden muss, wenn an den Traits-Namen _v angehängt wird. Das heißt, dass statt std::is_arithmetic<T>::value kürzer std::is_arithmetic_v<T> geschrieben werden kann. Und is_class<T>::value wird dann das kürzere is_class_v<T>.
In diesem Abschnitt geht es um das Verständnis, wie Type Traits funktionieren, nicht um eine vollständige (platzraubende) Darstellung. Auch sind Type Traits hauptsächlich für die Entwicklung von Bibliotheken wichtig, nicht für Programme, die Bibliotheken nutzen. Aus diesen Gründen wird im Folgenden nur eine kleine Auswahl der Type Traits vorgestellt.
// Einfache Typ-Kategorie template <class T> struct is_void; // void template <class T> struct is_null_pointer; // nullptr_t (Typ des Nullpointers) template <class T> struct is_integral; // Ganzzahltyp template <class T> struct is_floating_point; // Gleitkommazahltyp template <class T> struct is_array; // C-Array-Typ template <class T> struct is_pointer; // »roher« Zeiger template <class T> struct is_lvalue_reference; // Referenz auf L-Wert template <class T> struct is_rvalue_reference; // Referenz auf R-Wert template <class T> struct is_enum; // Aufzählungstyp template <class T> struct is_class; // Klasse template <class T> struct is_function; // Funktionstyp // zusammengesetzte Typ-Kategorie template <class T> struct is_reference; // L- oder R-Wert template <class T> struct is_arithmetic; // arithmetischer Typ template <class T> struct is_fundamental; // Grunddatentyp (alle ganzzahligen Typen, // alle Gleitkommatypen, alle Zeichentypen // und bool) // Typeigenschaften template <class B, class A> struct is_base_of; // A ist B oder von B abgeleitet template <class T> struct is_const; // const-Typ template <class T, class U> struct is_convertible; // T kann in U umgewandelt werden template <class T> struct is_polymorphic; // Klasse mit virtueller Methode template <class T> struct is_abstract; // abstrakte Klasse template <class T> struct is_final; // Klasse, von der nicht abgeleitet werden kann template <class T, class U> struct is_same; // T und U sind gleich template <class T> struct is_signed; // vorzeichenbehaftet template <class T> struct is_unsigned; // nicht vorzeichenbehaftet
Die Datei cppbuch/k13/typetraits/typrelation.cpp (nicht abgedruckt) zeigt am Beispiel von is_convertible, is_same und is_base_of, wie eine Anwendung aussehen kann.
13.5.3 | Abfrage numerischer Eigenschaften |
template <class T> struct alignment_of fragt die Ausrichtung an Speichergrenzen ab. Für einen Typ X gilt std::alignment_of<X>::value == alignof(X). Ein Beispiel finden Sie in der Datei cppbuch/k13/typetraits/alignof.cpp.
std::rank<X>::value gibt die Anzahl der Dimensionen zurück, wenn X ein Arraytyp ist, andernfalls 0. Auszug aus cppbuch/k13/typetraits/rank.cpp:
Hier wird das abkürzende _v verwendet, statt ::value zu schreiben. std::extent<X, N = 0>::value gibt die Anzahl der Elemente der Dimension N des Arraytyps X an, andernfalls ist der Wert 0. Auszug aus cppbuch/k13/typetraits/extent.cpp:
13.5.4 | Typumwandlungen |
Diese Klassen haben ein Attribut type, das den gewünschten Typ repräsentiert.
remove_reference entfernt die Referenz von einem Typ. Für eine Klasse X gilt:
std::is_same_v<X, std::remove_reference<X&>::type> == true
Wie funktioniert remove_reference? Das wird klar, wenn Sie eine mögliche Implementation ansehen:
Ob das Templateargument von remove_reference T, T& oder T&& ist, der deklarierte Typ type ist immer T.
Auf ähnliche Weise funktioniert remove_const. Damit gilt:
std::remove_const<const X>::type ist X.
std::remove_const<int* const>::type ist int*.
std::remove_const<const int*>::type ist const int*,
also dasselbe. In diesem Fall ist der Zeiger selbst nicht const, er verweist nur auf nicht veränderliche Zahlen.
Abkürzende Schreibweise mit _t
Analog zur Abkürzung mit _v für ::value kann _t für ::type genommen werden. Statt remove_const<int* const>::type kann daher kürzer remove_const_t<int* const> geschrieben werden usw.
13.5.5 | Auswahl weiterer Traits |
Die Auswahl begründet sich auf der häufigeren Verwendung. So wird decay in der möglichen Implementation von apply() benutzt [ISOC++, intseq.general].
decay liefert den zugrunde liegenden Typ durch Entfernen der Referenz- und der const-Eigenschaft. Damit gilt zum Beispiel:
Ein einfaches Template zur Steuerung der Übersetzung ist enable_if. Es ist etwa so definiert:
Die Wirkung: Falls sich der erste Template-Parameter x zur Compilationszeit als true erweist, kann mit enable_if<x, T>::type der Typ T definiert werden. Andernfalls existiert enable_if<x, T>::type nicht. Damit kann enable_if für die oben genannte SFINAE-Technik verwendet werden. Eine andere Möglichkeit ist das vollständige Scheitern der Übersetzung, wenn eine Bedingung nicht erfüllt wird. Dazu wird die Funktion summe() des obigen Listings 13.19 (Seite 525) wie folgt modifiziert:
1. Die static_assert-Anweisung wird gestrichen.
2. Der Rückgabetyp wird mithilfe von enable_if so definiert, dass er nur existiert, wenn die Elemente des Containers von einem arithmetischen Typ (Zahlen) sind. Wenn nicht, gibt es keinen Rückgabetyp und die Übersetzung schlägt fehl.
// Der Rückgabetyp muss existieren: template <typename Container> typename std::enable_if< std::is_arithmetic_v<typename Container::value_type>, // true, false? typename Container::value_type>::type // true: enable_if::type ist Container::value_type // false: enable_if::type existiert nicht summe(const Container &cont) { // Die Objekterzeugung gelingt nur bei existierendem Typ: typename Container::value_type ergebnis {}; for (auto wert : cont) { ergebnis += wert; } return ergebnis; } // #includes und main() wie in Listing 13.19 oben.
Tipp
Es ist einfacher, statt enable_if ein Concept zu nehmen. Concepts sind das Thema des nächsten Abschnitts 13.6.
Character Traits definieren Typen und Funktionen von Zeichentypen. Der Header string muss inkludiert werden. Hier seien nur zwei Funktionen erwähnt. eof() gibt den EOF-Wert (End Of File) für den betreffenden Zeichentyp zurück. Und die Funktion length(str) berechnet die Länge des C-Strings str. Im Unterschied zur Funktion strlen() (Header <cstring>) ist sie jedoch constexpr. Die Datei cppbuch/k13/typetraits/chartraits.cpp zeigt die Anwendung beider Funktionen.
Weitere Traits und Einzelheiten finden Sie in [VJG] und [ISOC++, meta]. Der Abschnitt 28.1.1 dieses Buchs zeigt die Anwendung von Traits mit Iteratoren.
13.6 | Concepts |
Wenn möglich, soll man schon zur Compilationszeit die Gültigkeit bestimmter Annahmen sicherstellen. In diesem Abschnitt geht es nicht um Annahmen über Werte, die zur Compilationszeit mit static_assert() überprüft werden können, sondern über die Zusicherung bestimmter Eigenschaften von Datentypen. Für den Compiler sind solche Zusicherungen hilfreich. Wenn eine notwendige Eigenschaft fehlt, kann die Übersetzung sofort abgebrochen werden. Die Vorteile: Zeitersparnis, weil die Übersetzung bei einem Fehler schneller scheitert, und bessere Verständlichkeit der Fehlermeldungen, weil die nicht gelungene Zusicherung dokumentiert und der Fehler sofort klar wird. Überdies wird die Länge der Fehlermeldungen deutlich reduziert. Für diese Art von Zusicherungen gibt es die Concepts. Mangels einer guten Übersetzung verwende ich nur den englischen Begriff.
Concept
Ein Concept ist eine Menge von Anforderungen an einen Template-Datentypen, der ein Name zugeordnet wird. Das Concept ist damit Teil der Template-Schnittstelle. Es wird zur Compilationszeit ausgewertet.
Am besten sieht man das an Beispielen, zuerst ohne und dann zum Vergleich mit einem Concept.
Listing 13.22 zeigt eine Funktion summe() zum Addieren aller Zahlen eines Vektors. Die Elemente des Vektors sind sinnvollerweise von einem der Datentypen int oder double und Verwandten dieser Typen (long, float usw.).
template <typename T> T summe(const std::vector<T>& vec) { T ergebnis {0}; for (auto summand : vec) { ergebnis += summand; } return ergebnis; }
Listing 13.23 zeigt die Anwendung der Summenfunktion.
std::vector<int> container {9, 2, 3, 4, 5, 10}; std::cout << "Summe˽=˽" << summe(container) << ’\n’;
So weit, so gut. Was ist aber, wenn der Vektor aus Strings besteht? Zum Beispiel:
Die Funktion summe() würde einfach »einszweidrei« zurückgeben. Mit einer Summe hat das nichts zu tun; es ist nur eine Verkettung. Damit der Compiler solche Fälle erkennen kann, braucht es ein Concept, das garantiert, dass nur Zahlen addiert werden. In Listing 13.24 wird es Zahl genannt. Das folgende Concept erfordert (englisch requires) erstens, dass die Operation + möglich ist. Da das auch für Strings gilt, wird auch die Operation - für die Subtraktion erlaubt – die ist auf Zahlen, aber nicht auf Strings anwendbar.
template <typename T> concept Zahl = requires(T w) { // Entwurf w + w ; w - w ; // nicht vollständig, siehe Text }; template <Zahl T> // T muss das Concept Zahl erfüllen T summe(const std::vector<T>& vec) { T ergebnis {0}; for (auto summand : vec) { ergebnis += summand; } return ergebnis; }
Der Versuch, Strings zu addieren, würde damit sofort zu einer Fehlermeldung des Compilers führen, etwa dieser Art (leicht gekürzt):
In die requires-Klausel werden Operationen hineingeschrieben, die mit dem Typ T übersetzbar sind. Die obige requires-Klausel ist nicht vollständig, weil andere numerische Operationen fehlen. Die Funktion summe() würde hier auch für eine Klasse funktionieren, die zwar + und - implementiert, aber nicht / und *. Für unser Beispiel reicht die zusätzliche Angabe der Subtraktion. In Listing 13.24 sehen Sie, wie Sie selbst ein Concept schreiben können. Besser ist es, vorhandene Type Traits der C++-Standardbibliothek zu nutzen:
#include <type_traits> // ... template <typename T> concept Zahl = std::is_arithmetic_v<T>;
Dateien zu diesem Beispiel finden Sie im Verzeichnis cppbuch/k13/concepts/summe der downloadbaren Beispiele.
Listing 13.26 zeigt eine Funktion sortieren() zum Sortieren eines Containers.
1 template <class Container> 2 void sortieren(Container& c) 3 { 4 for (auto i{0}; i < std::ssize(c); ++i) { 5 for (auto j = i; j < std::ssize(c); ++j) { 6 if (c[i] > c[j]) { // Vergleich, Zugriff mit [] 7 auto temp = c[i]; 8 c[i] = c[j]; 9 c[j] = temp; 10 } 11 } 12 } 13 }
Es wird nicht die Bibliotheksfunktion std::sort() verwendet, weil sie vermutlich irgendwann mit einem Concept ausgestattet sein wird und dann der Unterschied ohne und mit Concept nicht erkennbar wäre. Auch ist die Performance für den Zweck dieses Beispiels egal. Einige Anforderungen an die Elemente des zu sortierenden Containers sind sofort erkennbar, zum Beispiel, dass ein Vergleich mit > und eine Zuweisung möglich sein muss (Zeilen 6, 8 und 9). Hier soll es jedoch nur um die Anforderungen an den zu sortierenden Container gehen. Für den Algorithmus ist entscheidend, dass auf die Elemente mit dem []-Operator zugegriffen wird und dass der Zugriff wahlfrei (englisch random access) ist. Das heißt, dem []-Operator wird direkt die Adresse des Elements mitgegeben (Zeilen 6 bis 9). Das Listing 13.27 zeigt die Anwendung der Sortierfunktion.
std::vector<int> container {9, 2, 3, 4, 5, 10}; sortieren(container);
Was ist aber, wenn der Container kein Vektor ist, sondern eine Liste? Zum Beispiel:
Es würde eine Fehlermeldung geben, weil std::list keinen []-Operator hat. Ein []-Operator allein genügt aber nicht, er muss auch einen wahlfreien Zugriff ermöglichen. Das ist beim Vektor der Fall, nicht aber beim map-Container. Der wird nur aus diesem Grund hier vorab erwähnt; erst in Abschnitt 27.4.1 wird er ausführlich beschrieben. Ohne Concept erkennt der Compiler std::map nicht als fehlerhaft bezüglich der Sortierfunktion.
Daraus folgt, dass die Eigenschaft »wahlfreier Zugriff« von einem Concept gesichert werden sollte. Dass man eine Zahl auf einen Iterator für wahlfreien Zugriff addieren kann, um eine andere Position zu erhalten, wissen Sie aus Abschnitt 10.2. Das kann man sich zunutze machen, indem im Concept eine entsprechende Rechenoperation definiert wird. Nur wenn diese übersetzbar ist, ist das Concept gültig. Listing 13.28 zeigt das Concept und seine Anwendung.
template<typename Container> concept Sortierbar = requires(Container c) { // bessere Alternative s.u. c[0] < c[0] ; // vergleichbar und []-Operator c.begin() + 1 ; // wahlfreier Zugriff }; void sortieren(Sortierbar auto& c) // alternativ: template <Sortierbar T> void sortieren( T& c) { for (auto i{0}; i < std::ssize(c); ++i) { for (auto j = i; j < std::ssize(c); ++j) { if (c[j] < c[i]) { // Zugriff mit [] auto temp = c[i]; c[i] = c[j]; c[j] = temp; } } } }
Die Wirkung der Parameterliste Sortierbar auto& c kann man sich so vorstellen: Erst ermittelt der Compiler mit auto den Typ des Containers c anhand des Aufrufs der Funktion. Dann prüft er, ob der ermittelte Typ die Anforderungen des Concepts Sortierbar erfüllt.
Dateien zu diesem Beispiel finden Sie im Verzeichnis cppbuch/k13/concepts/sortieren der downloadbaren Beispiele. Da Sie nun wissen, wie Concepts im Prinzip funktionieren, sollten Sie möglichst wenige selbst schreiben, sondern sich auf die vorhandenen verlassen. So können Sie Sortierbar streichen und durch das existierende sortable ersetzen. Es müssen nur der Parameter der Funktion sortieren() und der Header angepasst werden:
Es geht oben und hier um das Verständnis, wie Concepts funktionieren, nicht um eine vollständige Darstellung. Auch sind Concepts wie Type Traits hauptsächlich für die Entwicklung von Bibliotheken wichtig, weniger für Programme, die Bibliotheken nutzen. Deswegen wird im Folgenden nur kurz auf die Konstruktion von Concepts mit Type Traits eingegangen und es werden wenige weitere vorgestellt. Der Header ist <concepts>, wenn nicht anders angegeben. Die in C++ vorgesehenen Concepts leiten sich oft aus Type Traits ab. So könnte same_as etwa so mit is_same<T, U> aus Tabelle 13.20 (Seite 526) implementiert werden:
is_same_v<T, U> ist dabei wie üblich dasselbe wie is_same<T, U>::value. Das Concept signed_integral für eine vorzeichenbehaftete Ganzzahl wird mit dem Concept für eine (negative oder positive) Ganzzahl gebildet:
Dies gilt auch für char, weil ein char-Zeichen in C++ eine 1-Byte-Ganzzahl ist. Die Nächsten sind selbsterklärend, besonders wenn die zugrunde liegenden Type Traits bekannt sind.
Andere Concepts beziehen sich auf die Konstruktion oder Zuweisung von Objekten:
LHS und RHS stehen für die linke bzw. rechte Seite einer Zuweisung (englisch left hand side bzw. right hand side). Es gibt auch Concepts für Iteratoren (Header <iterator>). Oben bei der Sortierfunktion wird der wahlfreie Zugriff erwähnt. Das Concept dazu ist:
Damit hätte das Concept Sortierbar in Listing 13.28 so definiert werden können:
Allerdings ist das oben schon erwähnte Concept sortable umfassender und deshalb zu bevorzugen. Weitere Concepts und Einzelheiten dazu finden Sie in [ISOC++, concepts.syn] und [ISOC++, iterator.concepts].