Constraints and concepts (since C++20)
Klassentemplates , Funktionstemplates (einschließlich generischer Lambdas ), und andere templatisierte Funktionen (typischerweise Mitglieder von Klassentemplates) können mit einer Constraint assoziiert sein, welche die Anforderungen an Template-Argumente spezifiziert, die verwendet werden können, um die am besten geeigneten Funktionsüberladungen und Templatespezialisierungen auszuwählen.
Benannte Mengen solcher Anforderungen werden Concepts genannt. Jedes Concept ist ein Prädikat, das zur Compilezeit ausgewertet wird und Teil der Schnittstelle eines Templates wird, wo es als Constraint verwendet wird:
#include <cstddef> #include <concepts> #include <functional> #include <string> // Deklaration des Konzepts „Hashable“, das von jedem Typ „T“ erfüllt wird // für den bei Werten „a“ vom Typ „T“ der Ausdruck std::hash<T>{}(a) // kompiliert und sein Ergebnis in std::size_t konvertierbar ist template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; struct meow {}; // Eingeschränkte C++20-Funktionsvorlage: template<Hashable T> void f(T) {} // // Alternative Möglichkeiten, dieselbe Einschränkung anzuwenden: // template<typename T> // requires Hashable<T> // void f(T) {} // // template<typename T> // void f(T) requires Hashable<T> {} // // void f(Hashable auto /* parameter-name */) {} int main() { using std::operator""s; f("abc"s); // OK, std::string erfüllt Hashable // f(meow{}); // Fehler: meow erfüllt Hashable nicht }
Verstöße gegen Constraints werden zur Kompilierzeit erkannt, früh im Template-Instanziierungsprozess, was zu leicht nachvollziehbaren Fehlermeldungen führt:
std::list<int> l = {3, -1, 10}; std::sort(l.begin(), l.end()); // Typische Compiler-Diagnose ohne Konzepte: // invalid operands to binary expression ('std::_List_iterator<int>' and // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 Zeilen Ausgabe ... // // Typische Compiler-Diagnose mit Konzepten: // error: cannot call std::sort with std::_List_iterator<int> // note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
Die Absicht von Concepts ist es, semantische Kategorien (Number, Range, RegularFunction) zu modellieren und nicht syntaktische Einschränkungen (HasPlus, Array). Gemäß ISO C++ Core Guideline T.20 ist „die Fähigkeit, bedeutungsvolle Semantik zu spezifizieren, ein bestimmendes Merkmal eines echten Concepts im Gegensatz zu einer syntaktischen Einschränkung.“
Inhaltsverzeichnis |
Konzepte
Ein Konzept ist eine benannte Menge von Anforderungen . Die Definition eines Konzepts muss im Namespace-Bereich erscheinen.
Die Definition eines Konzepts hat die Form
template <
template-parameter-list
>
|
|||||||||
| attr | - | Folge beliebig vieler Attribute |
// Konzept template<class T, class U> concept Derived = std::is_base_of<U, T>::value;
Konzepte können nicht rekursiv auf sich selbst verweisen und können nicht eingeschränkt werden:
template<typename T> concept V = V<T*>; // Fehler: rekursives Konzept template<class T> concept C1 = true; template<C1 T> concept Error1 = true; // Fehler: C1 T versucht eine Konzeptdefinition einzuschränken template<class T> requires C1<T> concept Error2 = true; // Fehler: die Requires-Klausel versucht ein Konzept einzuschränken
Explizite Instanziierungen, explizite Spezialisierungen oder partielle Spezialisierungen von Konzepten sind nicht erlaubt (die Bedeutung der ursprünglichen Definition einer Constraint darf nicht verändert werden).
Konzepte können in einem id-Ausdruck benannt werden. Der Wert des id-Ausdrucks ist true wenn der Constraint-Ausdruck erfüllt ist, und false andernfalls.
Konzepte können auch in einer Typ-Einschränkung benannt werden, als Teil von
In einer Type-Constraint nimmt ein Konzept ein Template-Argument weniger an, als seine Parameterliste erfordert, da der kontextuell abgeleitete Typ implizit als erstes Argument des Konzepts verwendet wird.
template<class T, class U> concept Derived = std::is_base_of<U, T>::value; template<Derived<Base> T> void f(T); // T wird durch Derived<T, Base> eingeschränkt
Einschränkungen
Eine Constraint ist eine Sequenz von logischen Operationen und Operanden, die Anforderungen an Template-Argumente spezifiziert. Sie können innerhalb von requires expressions oder direkt als Körper von Concepts erscheinen.
Es gibt drei (until C++26) vier (since C++26) Arten von Constraints:
|
4)
Faltungs-erweiterte Constraints
|
(since C++26) |
Die mit einer Deklaration assoziierte Constraint wird durch Normalisierung eines logischen UND-Ausdrucks bestimmt, dessen Operanden in folgender Reihenfolge stehen:
- der Constraint-Ausdruck, der für jeden eingeschränkten Typ-Template-Parameter oder konstanten Template-Parameter deklariert mit einem eingeschränkten Platzhaltertyp eingeführt wird, in der Reihenfolge ihres Erscheinens;
- der Constraint-Ausdruck in der requires -Klausel nach der Template-Parameterliste;
- der Constraint-Ausdruck, der für jeden Parameter mit eingeschränktem Platzhaltertyp in einer abgekürzten Funktions-Template-Deklaration eingeführt wird;
- der Constraint-Ausdruck in der nachgestellten requires -Klausel .
Diese Reihenfolge bestimmt die Reihenfolge, in der Einschränkungen bei der Überprüfung auf Erfüllung instanziiert werden.
Neudeklarationen
Eine eingeschränkte Deklaration darf nur mit derselben syntaktischen Form neu deklariert werden. Es ist keine Diagnose erforderlich:
// Diese ersten beiden Deklarationen von f sind in Ordnung template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // OK, Neudeklaration // Das Einfügen dieser dritten, logisch äquivalenten aber syntaktisch unterschiedlichen // Deklaration von f ist fehlerhaft, keine Diagnose erforderlich template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // Die folgenden beiden Deklarationen haben unterschiedliche Constraints: // die erste Deklaration hat Incrementable<T> && Decrementable<T> // die zweite Deklaration hat Decrementable<T> && Incrementable<T> // Obwohl sie logisch äquivalent sind. template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // fehlerhaft, keine Diagnose erforderlich
Konjunktionen
Die Konjunktion zweier Constraints wird durch Verwendung des
&&
Operators im Constraint-Ausdruck gebildet:
template<class T> concept Integral = std::is_integral<T>::value; template<class T> concept SignedIntegral = Integral<T> && std::is_signed<T>::value; template<class T> concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
`-Tags wurde gemäß den Anweisungen nicht übersetzt, da es sich um C++-Code handelt. Die HTML-Struktur und -Attribute wurden ebenfalls unverändert beibehalten.
Eine Konjunktion zweier Constraints ist nur dann erfüllt, wenn beide Constraints erfüllt sind. Konjunktionen werden von links nach rechts ausgewertet und kurzgeschlossen (wenn das linke Constraint nicht erfüllt ist, wird die Template-Argument-Substitution in das rechte Constraint nicht versucht: dies verhindert Fehler aufgrund von Substitution außerhalb des unmittelbaren Kontexts).
template<typename T> constexpr bool get_value() { return T::value; } template<typename T> requires (sizeof(T) > 1 && get_value<T>()) void f(T); // #1 void f(int); // #2 void g() { f('A'); // OK, ruft #2 auf. Beim Überprüfen der Constraints von #1, // wird 'sizeof(char) > 1' nicht erfüllt, daher wird get_value<T>() nicht überprüft }
Disjunktionen
Die Disjunktion zweier Constraints wird durch Verwendung des
||
Operators im Constraint-Ausdruck gebildet.
Eine Disjunktion zweier Constraints ist erfüllt, wenn eines der beiden Constraints erfüllt ist. Disjunktionen werden von links nach rechts ausgewertet und kurzgeschlossen (wenn das linke Constraint erfüllt ist, wird keine Template-Argument-Substitution in das rechte Constraint versucht).
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
Atomare Constraints
Eine atomare Constraint besteht aus einem Ausdruck E und einer Abbildung von den Template-Parametern, die innerhalb von E vorkommen, auf Template-Argumente, die die Template-Parameter der constrained entity betreffen, genannt ihre Parameterabbildung .
Atomare Constraints werden während der Constraint-Normalisierung gebildet. E ist niemals ein logisches UND- oder logisches ODER-Expression (diese bilden jeweils Konjunktionen und Disjunktionen).
Die Erfüllung einer atomaren Bedingung wird durch Substitution der Parametermapping- und Template-Argumente in den Ausdruck E überprüft. Falls die Substitution zu einem ungültigen Typ oder Ausdruck führt, ist die Bedingung nicht erfüllt. Andernfalls muss E nach etwaiger Lvalue-zu-Rvalue-Konvertierung ein prvalue-Konstantenausdruck vom Typ bool sein, und die Bedingung ist genau dann erfüllt, wenn dieser zu true ausgewertet wird.
Der Typ von E nach der Substitution muss exakt bool sein. Keine Konvertierung ist zulässig:
template<typename T> struct S { constexpr operator bool() const { return true; } }; template<typename T> requires (S<T>{}) void f(T); // #1 void f(int); // #2 void g() { f(0); // Fehler: S<int>{} hat nicht den Typ bool bei der Überprüfung von #1, // obwohl #2 eine bessere Übereinstimmung ist }
Zwei atomare Constraints gelten als identisch , wenn sie aus demselben Ausdruck auf Quelltextebene gebildet werden und ihre Parameterzuordnungen äquivalent sind.
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_cat = true; template<class T> concept Meowable = is_meowable<T>; template<class T> concept BadMeowableCat = is_meowable<T> && is_cat<T>; template<class T> concept GoodMeowableCat = Meowable<T> && is_cat<T>; template<Meowable T> void f1(T); // #1 template<BadMeowableCat T> void f1(T); // #2 template<Meowable T> void f2(T); // #3 template<GoodMeowableCat T> void f2(T); // #4 void g() { f1(0); // Fehler, mehrdeutig: // das is_meowable<T> in Meowable und BadMeowableCat bildet unterschiedliche atomare // Constraints, die nicht identisch sind (und sich daher nicht gegenseitig subsumieren) f2(0); // OK, ruft #4 auf, stärker eingeschränkt als #3 // GoodMeowableCat erhielt sein is_meowable<T> von Meowable }
Faltungs-erweiterte Constraints
Ein
faltungs-erweitertes Constraint
wird aus einem Constraint
Sei N die Anzahl der Elemente in den Pack-Erweiterungsparametern:
template <class T> concept A = std::is_move_constructible_v<T>; template <class T> concept B = std::is_copy_constructible_v<T>; template <class T> concept C = A<T> && B<T>; // in C++23, these two overloads of g() have distinct atomic constraints // that are not identical and so do not subsume each other: calls to g() are ambiguous // in C++26, the folds are expanded and constraint on overload #2 (both move and copy // required), subsumes constraint on overload #1 (just the move is required) template <class... T> requires (A<T> && ...) void g(T...); // #1 template <class... T> requires (C<T> && ...) void g(T...); // #2
|
(seit C++26) |
Constraint-Normalisierung
Constraint-Normalisierung ist der Prozess, der einen Constraint-Ausdruck in eine Sequenz von Konjunktionen und Disjunktionen atomarer Constraints transformiert. Die Normalform eines Ausdrucks ist wie folgt definiert:
- Die Normalform eines Ausdrucks ( E ) ist die Normalform von E .
- Die Normalform eines Ausdrucks E1 && E2 ist die Konjunktion der Normalformen von E1 und E2 .
- Die Normalform eines Ausdrucks E1 || E2 ist die Disjunktion der Normalformen von E1 und E2 .
-
Die Normalform eines Ausdrucks
C
<
A1, A2, ... , AN
>
, wobei
Cein Konzept benennt, ist die Normalform des Constraint-Ausdrucks vonC, nachdemA1,A2, ... ,ANfür die jeweiligen Template-Parameter vonCin den Parameterabbildungen jedes atomaren Constraints vonCsubstituiert wurden. Falls eine solche Substitution in den Parameterabbildungen zu einem ungültigen Typ oder Ausdruck führt, ist das Programm ill-formed, keine Diagnose erforderlich.
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // OK: normalisiert zur Disjunktion von // - T::value (mit Mapping T -> U*) und // - true (mit einem leeren Mapping). // Kein ungültiger Typ im Mapping, obwohl // T::value für alle Zeigertypen ill-formed ist template<typename V> concept C = B<V&>; // Normalisiert zur Disjunktion von // - T::value (mit Mapping T-> V&*) und // - true (mit einem leeren Mapping). // Ungültiger Typ V&* im Mapping gebildet => ill-formed NDR
|
(seit C++26) |
-
Die Normalform jedes anderen Ausdrucks
E
ist die atomare Constraint, deren Ausdruck
E
ist und deren Parameterabbildung die Identitätsabbildung ist. Dies umfasst alle
Fold-Ausdrücke
, selbst jene, die über die
&&- oder||-Operatoren falten.
Benutzerdefinierte Überladungen von
&&
oder
||
haben keine Auswirkung auf die Constraint-Normalisierung.
requires -Klauseln
Das Schlüsselwort requires wird verwendet, um eine requires Klausel einzuführen, die Einschränkungen für Template-Argumente oder für eine Funktionsdeklaration spezifiziert.
template<typename T> void f(T&&) requires Eq<T>; // kann als letztes Element eines Funktionsdeklarators erscheinen template<typename T> requires Addable<T> // oder direkt nach einer Template-Parameterliste T add(T a, T b) { return a + b; }
In diesem Fall muss das Schlüsselwort requires von einem konstanten Ausdruck gefolgt werden (daher ist es möglich, requires true zu schreiben), aber die Absicht ist, dass ein benanntes Konzept (wie im obigen Beispiel) oder eine Konjunktion/Disjunktion von benannten Konzepten oder ein requires-Ausdruck verwendet wird.
Der Ausdruck muss eine der folgenden Formen haben:
- Ein primärer Ausdruck , z.B. Swappable < T > , std:: is_integral < T > :: value , ( std:: is_object_v < Args > && ... ) , oder jeder geklammerte Ausdruck.
-
Eine Sequenz primärer Ausdrücke verbunden mit dem Operator
&&. -
Eine Sequenz der oben genannten Ausdrücke verbunden mit dem Operator
||.
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_purrable() { return true; } template<class T> void f(T) requires is_meowable<T>; // OK template<class T> void g(T) requires is_purrable<T>(); // Fehler, is_purrable<T>() ist kein primärer Ausdruck template<class T> void h(T) requires (is_purrable<T>()); // OK
Partielle Ordnung von Constraints
Vor jeder weiteren Analyse werden Constraints normalisiert durch Substitution des Rumpfes jedes benannten Konzepts und jedes requires-Ausdrucks bis nur noch eine Sequenz von Konjunktionen und Disjunktionen atomarer Constraints übrig bleibt.
Eine Nebenbedingung
P
wird als
subsumierend
für Nebenbedingung
Q
bezeichnet, wenn bewiesen werden kann, dass
P
impliziert
Q
bis auf die Identität atomarer Nebenbedingungen in P und Q. (Typen und Ausdrücke werden nicht auf Äquivalenz analysiert:
N > 0
subsumiert nicht
N >= 0
).
Konkret wird zunächst
P
in disjunktive Normalform umgewandelt und
Q
in konjunktive Normalform.
P
subsumiert
Q
genau dann, wenn:
-
jede Disjunktionsklausel in der disjunktiven Normalform von
Psubsumiert jede Konjunktionsklausel in der konjunktiven Normalform vonQ, wobei -
eine Disjunktionsklausel eine Konjunktionsklausel genau dann subsumiert, wenn es eine atomare Constraint
Uin der Disjunktionsklausel und eine atomare ConstraintVin der Konjunktionsklausel gibt, sodassUVsubsumiert; -
eine atomare Constraint
Aeine atomare ConstraintBgenau dann subsumiert, wenn sie gemäß den oben beschriebenen Regeln identisch sind.
|
(seit C++26) |
Die Subsumptionsbeziehung definiert eine partielle Ordnung von Constraints, die verwendet wird, um zu bestimmen:
- der beste geeignete Kandidat für eine Nicht-Template-Funktion in der Überlagerungsauflösung
- die Adresse einer Nicht-Template-Funktion in einem Überlagerungssatz
- die beste Übereinstimmung für ein Template-Template-Argument
- partielle Ordnung von Klassentemplate-Spezialisierungen
- partielle Ordnung von Funktionstemplates
|
Dieser Abschnitt ist unvollständig
Grund: Rückverweise von oben hierher |
Wenn Deklarationen
D1
und
D2
constrained sind und
D1
's assoziierte Constraints die von
D2
subsumieren (oder wenn
D2
unconstrained ist), dann wird
D1
als
mindestens so constrained
wie
D2
bezeichnet. Wenn
D1
mindestens so constrained wie
D2
ist und
D2
nicht mindestens so constrained wie
D1
ist, dann ist
D1
mehr constrained
als
D2
.
Wenn alle folgenden Bedingungen erfüllt sind, ist eine Nicht-Template-Funktion
F1
mehr teilweise geordnet eingeschränkt
als eine Nicht-Template-Funktion
F2
:
- Sie haben dieselbe Parameter-Typen-Liste , unter Auslassung der Typen von explicit object parameters (since C++23) .
- Wenn es sich um Member-Funktionen handelt, sind beide direkte Member derselben Klasse.
- Wenn beide nicht-statische Member-Funktionen sind, haben sie dieselben Typen für ihre Objektparameter.
-
F1ist stärker eingeschränkt alsF2.
template<typename T> concept Decrementable = requires(T t) { --t; }; template<typename T> concept RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIterator subsumiert Decrementable, aber nicht umgekehrt template<Decrementable T> void f(T); // #1 template<RevIterator T> void f(T); // #2, stärker eingeschränkt als #1 f(0); // int erfüllt nur Decrementable, wählt #1 f((int*)0); // int* erfüllt beide Constraints, wählt #2 als stärker eingeschränkt template<class T> void g(T); // #3 (uneingeschränkt) template<Decrementable T> void g(T); // #4 g(true); // bool erfüllt Decrementable nicht, wählt #3 g(0); // int erfüllt Decrementable, wählt #4 weil es stärker eingeschränkt ist template<typename T> concept RevIterator2 = requires(T t) { --t; *t; }; template<Decrementable T> void h(T); // #5 template<RevIterator2 T> void h(T); // #6 h((int*)0); // mehrdeutig
Hinweise
| Feature-Test-Makro | Wert | Std | Feature |
|---|---|---|---|
__cpp_concepts
|
201907L
|
(C++20) | Constraints |
202002L
|
(C++20) | Bedingt triviale spezielle Elementfunktionen |
Schlüsselwörter
Fehlerberichte
Die folgenden verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | Angewendet auf | Verhalten wie veröffentlicht | Korrektes Verhalten |
|---|---|---|---|
| CWG 2428 | C++20 | konnte Attribute nicht auf Concepts anwenden | erlaubt |
Siehe auch
| Requires-Ausdruck (C++20) | liefert einen prvalue-Ausdruck vom Typ bool , der die Constraints beschreibt |