Namensräume
Varianten
Aktionen

Constraints und Konzepte (seit C++20)

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 
 
 
 

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 >

concept konzept-name attr (optional) = constraint-ausdruck;

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:

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

  1. 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;
  2. der Constraint-Ausdruck in der requires-Klausel nach der Template-Parameterliste;
  3. der für jeden Parameter mit einem eingeschränkten Platzhaltertyp in einer abgekürzten Funktionstemplate-Deklaration deklarierte Constraint-Ausdruck;
  4. 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 Constraints

Ein falt-erweiterter Constraint wird aus einem Constraint C und einem Faltoperator (entweder && oder ||) gebildet. Ein falt-erweiterter Constraint ist eine Pack-Erweiterung.

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

  • Wenn die Pack-Erweiterung ungültig ist (z. B. Erweiterung von Packs unterschiedlicher Größe), ist der falt-erweiterte Constraint nicht erfüllt.
  • Wenn N 0 ist, ist der falt-erweiterte Constraint erfüllt, wenn der Faltoperator && ist, oder nicht erfüllt, wenn der Faltoperator || ist.
  • Für einen falt-erweiterten Constraint mit einem positiven N wird für jedes i in [1N] jeder Pack-Erweiterungsparameter durch das entsprechende i-te Element in aufsteigender Reihenfolge ersetzt.
  • Bei falt-erweiterten Constraints, deren Faltoperator && ist, führt eine Verletzung von C durch die Ersetzung des j-ten Elements dazu, dass der falt-erweiterte Constraint nicht erfüllt ist. In diesem Fall findet keine Substitution für irgendein i größer als j statt. Andernfalls ist der falt-erweiterte Constraint erfüllt.
  • Bei falt-erweiterten Constraints, deren Faltoperator || ist, führt die Erfüllung von C durch die Ersetzung des j-ten Elements dazu, dass der falt-erweiterte Constraint erfüllt ist. In diesem Fall findet keine Substitution für irgendein i größer als j statt. Andernfalls ist der falt-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)

[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 C ein Konzept benennt, ist die Normalform des Constraint-Ausdrucks von C, nach der Substitution von A1, A2, ... , AN für Cs jeweilige Template-Parameter in den Parameterabbildungen jedes atomaren Constraints von C. 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
  • Die Normalform von Ausdrücken (E && ...) und (... && E) ist ein falt-erweiterter Constraint, wobei C die Normalform von E und der Faltoperator && ist.
  • Die Normalform von Ausdrücken (E || ...) und (... || E) ist ein falt-erweiterter Constraint, wobei C die Normalform von E und der Faltoperator || ist.
  • Die Normalformen von Ausdrücken (E1 && ... && E2) und (E1 || ... || E2) sind die Normalformen von
  • (E1 && ...) && E2 und (E1 || ...) || E2, bzw., wenn E1 einen nicht expandierten Pack enthält, oder
  • E1 && (... && E2) und E1 || (... || E2), bzw. andernfalls.
(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 P jede konjunktive Klausel in der konjunktiven Normalform von Q subsumiert, wobei
  • eine disjunktive Klausel eine konjunktive Klausel subsumiert, genau dann, wenn es einen atomaren Constraint U in der disjunktiven Klausel und einen atomaren Constraint V in der konjunktiven Klausel gibt, so dass U V subsumiert;
  • ein atomarer Constraint A einen atomaren Constraint B subsumiert, genau dann, wenn sie gemäß den oben beschriebenen Regeln identisch sind.
  • Ein falt-erweiterter Constraint A subsumiert einen anderen falt-erweiterten Constraint B, wenn sie den gleichen Faltoperator haben, der Constraint C von A den von B subsumiert und beide C einen äquivalenten nicht expandierten Pack enthalten.
(seit C++26)

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

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.
  • 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 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

concept, requires, typename

[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.[edit]