Constraints und Konzepte (seit C++20)
Klassentemplates, Funktionstemplates (einschließlich generische Lambdas) und andere templatisierten Funktionen (typischerweise Mitglieder von Klassentemplates) können mit einer Constraint assoziiert sein, die die Anforderungen an Template-Argumente spezifiziert. Dies kann verwendet werden, um die am besten geeigneten Funktionsüberladungen und Templatespezialisierungen auszuwählen.
Benannte Mengen solcher Anforderungen werden Konzepte genannt. Jedes Konzept 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> // Declaration of the concept “Hashable”, which is satisfied by any type “T” // such that for values “a” of type “T”, the expression std::hash<T>{}(a) // compiles and its result is convertible to std::size_t template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; struct meow {}; // Constrained C++20 function template: template<Hashable T> void f(T) {} // // Alternative ways to apply the same constraint: // 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 satisfies Hashable // f(meow{}); // Error: meow does not satisfy Hashable }
Verletzungen von Constraints werden zur Compilezeit, früh im Template-Instanziierungsprozess, erkannt, was zu leicht verständlichen Fehlermeldungen führt.
std::list<int> l = {3, -1, 10}; std::sort(l.begin(), l.end()); // Typical compiler diagnostic without concepts: // invalid operands to binary expression ('std::_List_iterator<int>' and // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 lines of output ... // // Typical compiler diagnostic with concepts: // error: cannot call std::sort with std::_List_iterator<int> // note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
Die Absicht von Konzepten ist es, semantische Kategorien (Zahl, Bereich, reguläre Funktion) anstelle von syntaktischen Einschränkungen (HatPlus, Array) zu modellieren. Gemäß der ISO C++ Core Guideline T.20 ist "Die Fähigkeit, aussagekräftige Semantik zu spezifizieren, ein definierendes Merkmal eines echten Konzepts im Gegensatz zu einer syntaktischen Einschränkung."
Inhalt |
[edit] Konzepte
Ein Konzept ist eine benannte Menge von Anforderungen. Die Definition eines Konzepts muss im Namespace-Scope erscheinen.
Die Definition eines Konzepts hat die Form
template < template-parameter-liste >
|
|||||||||
| attr | - | Sequenz einer beliebigen Anzahl von Attributen |
// concept template<class T, class U> concept Derived = std::is_base_of<U, T>::value;
Konzepte können sich nicht rekursiv auf sich selbst beziehen und können nicht eingeschränkt werden.
template<typename T> concept V = V<T*>; // error: recursive concept template<class T> concept C1 = true; template<C1 T> concept Error1 = true; // Error: C1 T attempts to constrain a concept definition template<class T> requires C1<T> concept Error2 = true; // Error: the requires clause attempts to constrain a concept
Explizite Instanziierungen, explizite Spezialisierungen oder partielle Spezialisierungen von Konzepten sind nicht erlaubt (die Bedeutung der ursprünglichen Definition eines Constraints kann nicht geä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-Constraint benannt werden, als Teil von
In einer Typ-Constraint benötigt ein Konzept ein Template-Argument weniger als seine Parameterliste verlangt, 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 is constrained by Derived<T, Base>
[edit] Constraints
Ein Constraint ist eine Sequenz von logischen Operationen und Operanden, die Anforderungen an Template-Argumente spezifiziert. Sie können innerhalb von requires-Ausdrücken oder direkt als Körper von Konzepten erscheinen.
Es gibt drei(bis C++26)vier(seit C++26) Arten von Constraints:
|
4) falt-erweiterte Constraints
|
(seit C++26) |
Der mit einer Deklaration assoziierte Constraint wird durch Normalisierung eines logischen AND-Ausdrucks bestimmt, dessen Operanden in folgender Reihenfolge stehen:
- der für jeden eingeschränkten Typ-Template-Parameter oder nicht-Typ-Template-Parameter deklarierte Constraint-Ausdruck mit einem eingeschränkten Platzhaltertyp, in der Reihenfolge ihres Erscheinens;
- der Constraint-Ausdruck in der requires-Klausel nach der Template-Parameterliste;
- der für jeden Parameter mit einem eingeschränkten Platzhaltertyp in einer abgekürzten Funktionstemplate-Deklaration deklarierte Constraint-Ausdruck;
- der Constraint-Ausdruck in der nachgestellten requires-Klausel.
Diese Reihenfolge bestimmt die Reihenfolge, in der Constraints bei der Überprüfung auf Erfüllung instanziiert werden.
[edit] Neudeklarationen
Eine eingeschränkte Deklaration kann nur mit der gleichen syntaktischen Form neu deklariert werden. Keine Diagnose ist erforderlich.
// These first two declarations of f are fine template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // OK, redeclaration // Inclusion of this third, logically-equivalent-but-syntactically-different // declaration of f is ill-formed, no diagnostic required template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // The following two declarations have different constraints: // the first declaration has Incrementable<T> && Decrementable<T> // the second declaration has Decrementable<T> && Incrementable<T> // Even though they are logically equivalent. template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // ill-formed, no diagnostic required
[edit] Konjunktionen
Die Konjunktion zweier Constraints wird durch die 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>;
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 der linke Constraint nicht erfüllt ist, wird die Template-Argument-Substitution in den rechten 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, calls #2. When checking the constraints of #1, // 'sizeof(char) > 1' is not satisfied, so get_value<T>() is not checked }
[edit] Disjunktionen
Die Disjunktion zweier Constraints wird durch die Verwendung des ||-Operators im Constraint-Ausdruck gebildet.
Eine Disjunktion zweier Constraints ist dann erfüllt, wenn einer der Constraints erfüllt ist. Disjunktionen werden von links nach rechts ausgewertet und kurzgeschlossen (wenn der linke Constraint erfüllt ist, wird die Template-Argument-Substitution in den rechten Constraint nicht versucht).
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
[edit] Atomare Constraints
Ein atomarer Constraint besteht aus einem Ausdruck E und einer Abbildung der Template-Parameter, die in E vorkommen, auf Template-Argumente, die die Template-Parameter der eingeschränkten Entität betreffen, genannt seine Parameterabbildung.
Atomare Constraints werden während der Constraint-Normalisierung gebildet. E ist niemals ein logischer AND- oder logischer OR-Ausdruck (diese bilden Konjunktionen bzw. Disjunktionen).
Die Erfüllung eines atomaren Constraints wird durch Substitution der Parameterabbildung und der Template-Argumente in den Ausdruck E überprüft. Wenn die Substitution zu einem ungültigen Typ oder Ausdruck führt, ist der Constraint nicht erfüllt. Andernfalls muss E nach jeglicher Lvalue-zu-Rvalue-Konvertierung ein prvalue Konstantausdruck vom Typ bool sein, und der Constraint ist genau dann erfüllt, wenn er zu true ausgewertet wird.
Der Typ von E muss nach der Substitution exakt bool sein. Es sind keine Konvertierungen 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); // error: S<int>{} does not have type bool when checking #1, // even though #2 is a better match }
Zwei atomare Constraints gelten als identisch, wenn sie aus demselben Ausdruck auf Quellcodeebene gebildet werden und ihre Parameterabbildungen ä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); // error, ambiguous: // the is_meowable<T> in Meowable and BadMeowableCat forms distinct atomic // constraints that are not identical (and so do not subsume each other) f2(0); // OK, calls #4, more constrained than #3 // GoodMeowableCat got its is_meowable<T> from Meowable }
Falt-erweiterte ConstraintsEin falt-erweiterter 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) |
[edit] Constraint-Normalisierung
Constraint-Normalisierung ist der Prozess, der einen Constraint-Ausdruck in eine Sequenz von Konjunktionen und Disjunktionen von atomaren Constraints umwandelt. 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, nach der Substitution vonA1,A2, ... ,ANfürCs jeweilige Template-Parameter in den Parameterabbildungen jedes atomaren Constraints vonC. Wenn eine solche Substitution in die Parameterabbildungen zu einem ungültigen Typ oder Ausdruck führt, ist das Programm schlecht geformt, keine Diagnose erforderlich.
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // OK: normalized to the disjunction of // - T::value (with mapping T -> U*) and // - true (with an empty mapping). // No invalid type in mapping even though // T::value is ill-formed for all pointer types template<typename V> concept C = B<V&>; // Normalizes to the disjunction of // - T::value (with mapping T-> V&*) and // - true (with an empty mapping). // Invalid type V&* formed in mapping => ill-formed NDR
|
(seit C++26) |
- Die Normalform jedes anderen Ausdrucks E ist der atomare Constraint, dessen Ausdruck E und dessen Parameterabbildung die Identitätsabbildung ist. Dies schließt alle Falt-Ausdrücke ein, auch diejenigen, die über die Operatoren
&&oder||falten.
Benutzerdefinierte Überladungen von && oder || haben keinen Einfluss auf die Constraint-Normalisierung.
[edit] requires-Klauseln
Das Schlüsselwort requires wird verwendet, um eine requires-Klausel einzuführen, die Constraints auf Template-Argumente oder auf eine Funktionsdeklaration spezifiziert.
template<typename T> void f(T&&) requires Eq<T>; // can appear as the last element of a function declarator template<typename T> requires Addable<T> // or right after a template parameter list 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 benannter Konzepte oder ein requires-Ausdruck verwendet wird.
Der Ausdruck muss eine der folgenden Formen haben:
- Ein Primärausdruck, z. B. Swappable<T>, std::is_integral<T>::value, (std::is_object_v<Args> && ...), oder ein beliebiger geklammerter Ausdruck.
- Eine Sequenz von Primärausdrücken, verbunden mit dem Operator
&&. - Eine Sequenz von oben genannten Ausdrücken, 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>(); // error, is_purrable<T>() is not a primary expression template<class T> void h(T) requires (is_purrable<T>()); // OK
[edit] Partielle Ordnung von Constraints
Vor jeder weiteren Analyse werden Constraints normalisiert, indem der Körper jedes benannten Konzepts und jedes requires-Ausdrucks substituiert wird, bis nur noch eine Sequenz von Konjunktionen und Disjunktionen von atomaren Constraints übrig bleibt.
Ein Constraint P heißt subsumierend für Constraint Q, wenn bewiesen werden kann, dass P Q impliziert (bis auf die Identität atomarer Constraints in P und Q). (Typen und Ausdrücke werden nicht auf Äquivalenz analysiert: N > 0 subsumiert nicht N >= 0).
Genauer gesagt wird zuerst P in die disjunktive Normalform und Q in die konjunktive Normalform umgewandelt. P subsumiert Q genau dann, wenn
- jede disjunktive Klausel in der disjunktiven Normalform von
Pjede konjunktive Klausel in der konjunktiven Normalform vonQsubsumiert, wobei - eine disjunktive Klausel eine konjunktive Klausel subsumiert, genau dann, wenn es einen atomaren Constraint
Uin der disjunktiven Klausel und einen atomaren ConstraintVin der konjunktiven Klausel gibt, so dassUVsubsumiert; - ein atomarer Constraint
Aeinen atomaren ConstraintBsubsumiert, genau dann, wenn sie gemäß den oben beschriebenen Regeln identisch sind.
|
(seit C++26) |
Die Subsumptionsbeziehung definiert eine partielle Ordnung von Constraints, die verwendet wird, um
- den besten zulässigen Kandidaten für eine Nicht-Template-Funktion in der Überladungsauflösung
- die Adresse einer Nicht-Template-Funktion in einer Überladungsmenge
- die beste Übereinstimmung für ein Template-Template-Argument
- die partielle Ordnung von Klassentemplatesspezialisierungen
- die partielle Ordnung von Funktionstemplates zu bestimmen.
| Dieser Abschnitt ist unvollständig Grund: Rückverweise von oben nach hier. |
Wenn Deklarationen D1 und D2 eingeschränkt sind und D1s zugeordnete Constraints D2s zugeordnete Constraints subsumieren (oder wenn D2 unbeschränkt ist), dann heißt D1 mindestens so stark eingeschränkt wie D2. Wenn D1 mindestens so stark eingeschränkt ist wie D2 und D2 nicht mindestens so stark eingeschränkt ist wie D1, dann ist D1 stärker eingeschränkt als D2.
Wenn alle folgenden Bedingungen erfüllt sind, ist eine Nicht-Template-Funktion F1 partieller-geordnet-eingeschränkter als eine Nicht-Template-Funktion F2:
- Sie haben die gleiche Parameter-Typ-Liste, wobei die Typen von expliziten Objektparametern weggelassen werden(seit C++23).
- Wenn es sich um Klassenmember-Funktionen handelt, sind beide direkte Mitglieder derselben Klasse.
- Wenn beide nicht-statische Klassenmember-Funktionen sind, haben sie die gleichen 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 subsumes Decrementable, but not the other way around template<Decrementable T> void f(T); // #1 template<RevIterator T> void f(T); // #2, more constrained than #1 f(0); // int only satisfies Decrementable, selects #1 f((int*)0); // int* satisfies both constraints, selects #2 as more constrained template<class T> void g(T); // #3 (unconstrained) template<Decrementable T> void g(T); // #4 g(true); // bool does not satisfy Decrementable, selects #3 g(0); // int satisfies Decrementable, selects #4 because it is more constrained 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); // ambiguous
[edit] Anmerkungen
| Feature-Testmakro | Wert | Std | Feature |
|---|---|---|---|
__cpp_concepts |
201907L |
(C++20) | Constraints |
202002L |
(C++20) | Bedingt triviale spezielle Member-Funktionen |
[edit] Schlüsselwörter
[edit] Defect Reports
Die folgenden Verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | angewendet auf | Verhalten wie veröffentlicht | Korrigiertes Verhalten |
|---|---|---|---|
| CWG 2428 | C++20 | konnten keine Attribute auf Konzepte anwenden | erlaubt |
[edit] Siehe auch
| Requires-Ausdruck(C++20) | liefert einen prvalue-Ausdruck vom Typ bool, der die Constraints beschreibt. |