Namespaces
Variants

Constraints and concepts (since C++20)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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 >

concept concept-name attr  (optional) = constraint-expression ;

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:

1) Konjunktionen
2) Disjunktionen
3) atomare 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:

  1. 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;
  2. der Constraint-Ausdruck in der requires -Klausel nach der Template-Parameterliste;
  3. der Constraint-Ausdruck, der für jeden Parameter mit eingeschränktem Platzhaltertyp in einer abgekürzten Funktions-Template-Deklaration eingeführt wird;
  4. 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>;
**Anmerkung:** Der gesamte Code innerhalb der `
`-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 C und einem Faltungsoperator (entweder && oder || ) gebildet. Ein faltungs-erweitertes Constraint ist eine Pack-Erweiterung .

Sei N die Anzahl der Elemente in den Pack-Erweiterungsparametern:

  • Wenn die Pack-Erweiterung ungültig ist (z.B. durch Erweiterung von Packs unterschiedlicher Größe), ist das faltungs-erweiterte Constraint nicht erfüllt.
  • Wenn N gleich 0 ist, ist das faltungs-erweiterte Constraint erfüllt, wenn der Faltungsoperator && ist, oder nicht erfüllt, wenn der Faltungsoperator || ist.
  • Für ein faltungs-erweitertes Constraint mit einem positiven N , wird für jedes i in [ 1 , N ] jeder Pack-Erweiterungsparameter durch das entsprechende i -te Element in aufsteigender Reihenfolge ersetzt:
  • Für faltungs-erweiterte Constraints, deren Faltungsoperator && ist: Wenn der Ersatz des j -ten Elements C verletzt, ist das faltungs-erweiterte Constraint nicht erfüllt. In diesem Fall findet keine Substitution für irgendein i größer als j statt. Andernfalls ist das faltungs-erweiterte Constraint erfüllt.
  • Für faltungs-erweiterte Constraints, deren Faltungsoperator || ist: Wenn der Ersatz des j -ten Elements C erfüllt, ist das faltungs-erweiterte Constraint erfüllt. In diesem Fall findet keine Substitution für irgendein i größer als j statt. Andernfalls ist das faltungs-erweiterte Constraint nicht erfüllt.


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 C ein Konzept benennt, ist die Normalform des Constraint-Ausdrucks von C , nachdem A1 , A2 , ... , AN für die jeweiligen Template-Parameter von C in den Parameterabbildungen jedes atomaren Constraints von C substituiert 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
  • Die Normalform der Ausdrücke ( E && ... ) und ( ... && E ) ist eine Falt-erweiterte Constraint, wobei C die Normalform von E ist und der Faltoperator && lautet.
  • Die Normalform der Ausdrücke ( E || ... ) und ( ... || E ) ist eine Falt-erweiterte Constraint, wobei C die Normalform von E ist und der Faltoperator || lautet.
  • Die Normalformen der Ausdrücke ( E1 && ... && E2 ) und ( E1 || ... || E2 ) sind die Normalformen von
  • ( E1 && ... ) && E2 und ( E1 || ... ) || E2 entsprechend, falls E1 ein unerweitertes Pack enthält, oder
  • E1 && ( ... && E2 ) und E1 || ( ... || E2 ) entsprechend andernfalls.
(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 P subsumiert jede Konjunktionsklausel in der konjunktiven Normalform von Q , wobei
  • eine Disjunktionsklausel eine Konjunktionsklausel genau dann subsumiert, wenn es eine atomare Constraint U in der Disjunktionsklausel und eine atomare Constraint V in der Konjunktionsklausel gibt, sodass U V subsumiert;
  • eine atomare Constraint A eine atomare Constraint B genau dann subsumiert, wenn sie gemäß den oben beschriebenen Regeln identisch sind.
  • Eine Faltung erweiterte Einschränkung A subsumiert eine andere Faltung erweiterte Einschränkung B , wenn sie denselben Faltungsoperator haben, die Einschränkung C von A die von B subsumiert und beide C ein äquivalentes nicht erweitertes Paket enthalten.
(seit C++26)

Die Subsumptionsbeziehung definiert eine partielle Ordnung von Constraints, die verwendet wird, um zu bestimmen:

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.
  • F1 ist stärker eingeschränkt als F2 .
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

concept , requires , typename

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