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

Modèles de classe , modèles de fonction (y compris les lambdas génériques ), et autres fonctions template (généralement membres de modèles de classe) peuvent être associés à une contrainte  , qui spécifie les exigences sur les arguments template, pouvant être utilisée pour sélectionner les surcharges de fonction et spécialisations de template les plus appropriées.

Les ensembles nommés de telles exigences sont appelés concepts  . Chaque concept est un prédicat, évalué à la compilation, et devient une partie de l'interface d'un template où il est utilisé comme contrainte :

#include <cstddef>
#include <concepts>
#include <functional>
#include <string>
// Déclaration du concept « Hashable », qui est satisfait par tout type « T »
// tel que pour les valeurs « a » de type « T », l'expression std::hash<T>{}(a)
// compile et son résultat est convertible en std::size_t
template<typename T>
concept Hashable = requires(T a)
{
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
struct meow {};
// Fonction template C++20 contrainte :
template<Hashable T>
void f(T) {}
//
// Autres façons d'appliquer la même contrainte :
// 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 satisfait Hashable
    // f(meow{}); // Erreur : meow ne satisfait pas Hashable
}

Les violations des contraintes sont détectées au moment de la compilation, tôt dans le processus d'instanciation des templates, ce qui conduit à des messages d'erreur faciles à suivre :

std::list<int> l = {3, -1, 10};
std::sort(l.begin(), l.end()); 
// Diagnostic typique du compilateur sans concepts :
// invalid operands to binary expression ('std::_List_iterator<int>' and
// 'std::_List_iterator<int>')
//                           std::__lg(__last - __first) * 2);
//                                     ~~~~~~ ^ ~~~~~~~
// ... 50 lignes de sortie ...
//
// Diagnostic typique du compilateur avec concepts :
// error: cannot call std::sort with std::_List_iterator<int>
// note:  concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied

L'intention des concepts est de modéliser des catégories sémantiques (Number, Range, RegularFunction) plutôt que des restrictions syntaxiques (HasPlus, Array). Conformément aux directives fondamentales ISO C++ T.20 , « La capacité à spécifier une sémantique significative est une caractéristique déterminante d'un véritable concept, par opposition à une contrainte syntaxique. »

Table des matières

Concepts

Un concept est un ensemble nommé de exigences . La définition d'un concept doit apparaître au niveau de la portée de l'espace de noms.

La définition d'un concept a la forme

template < liste-de-paramètres-de-modèle >

concept nom-du-concept attr  (optionnel) = expression-de-contrainte ;

attr - séquence d'un nombre quelconque d' attributs
// concept
template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;

Les concepts ne peuvent pas se référer récursivement à eux-mêmes et ne peuvent pas être contraints :

template<typename T>
concept V = V<T*>; // erreur : concept récursif
template<class T>
concept C1 = true;
template<C1 T>
concept Error1 = true; // Erreur : C1 T tente de contraindre une définition de concept
template<class T> requires C1<T>
concept Error2 = true; // Erreur : la clause requires tente de contraindre un concept

Les instanciations explicites, les spécialisations explicites ou les spécialisations partielles de concepts ne sont pas autorisées (la signification de la définition originale d'une contrainte ne peut pas être modifiée).

Les concepts peuvent être nommés dans une expression d'identifiant. La valeur de l'expression d'identifiant est true si l'expression de contrainte est satisfaite, et false dans le cas contraire.

Les concepts peuvent également être nommés dans une contrainte de type, comme partie de

Dans une contrainte de type , un concept prend un argument de template de moins que ce que sa liste de paramètres exige, car le type déduit contextuellement est implicitement utilisé comme premier argument du concept.

template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;
template<Derived<Base> T>
void f(T); // T est contraint par Derived<T, Base>

Contraintes

Une contrainte est une séquence d'opérations logiques et d'opérandes qui spécifie des exigences sur les arguments template. Elles peuvent apparaître dans requires expressions ou directement comme corps de concepts.

Il existe trois (jusqu'à C++26) quatre (depuis C++26) types de contraintes :

1) conjonctions
2) disjonctions
3) contraintes atomiques
4) contraintes dépliées
(depuis C++26)

La contrainte associée à une déclaration est déterminée en normalisant une expression logique ET dont les opérandes sont dans l'ordre suivant :

  1. l'expression de contrainte introduite pour chaque paramètre de template de type contraint ou paramètre de template constant déclaré avec un type de substitution contraint , dans l'ordre d'apparition ;
  2. l'expression de contrainte dans la requires clause après la liste des paramètres du template ;
  3. l'expression de contrainte introduite pour chaque paramètre avec type de substitution contraint dans une déclaration de template de fonction abrégé ;
  4. l'expression de contrainte dans la requires clause finale.

Cet ordre détermine l'ordre dans lequel les contraintes sont instanciées lors de la vérification de la satisfaction.

Redéclarations

Une déclaration contrainte ne peut être redéclarée qu'en utilisant la même forme syntaxique. Aucun diagnostic n'est requis :

// Ces deux premières déclarations de f sont correctes
template<Incrementable T>
void f(T) requires Decrementable<T>;
template<Incrementable T>
void f(T) requires Decrementable<T>; // OK, redéclaration
// L'inclusion de cette troisième déclaration de f, logiquement équivalente mais syntaxiquement différente
// est mal formée, aucun diagnostic requis
template<typename T>
    requires Incrementable<T> && Decrementable<T>
void f(T);
// Les deux déclarations suivantes ont des contraintes différentes :
// la première déclaration a Incrementable<T> && Decrementable<T>
// la seconde déclaration a Decrementable<T> && Incrementable<T>
// Même si elles sont logiquement équivalentes.
template<Incrementable T> 
void g(T) requires Decrementable<T>;
template<Decrementable T> 
void g(T) requires Incrementable<T>; // mal formée, aucun diagnostic requis

Conjonctions

La conjonction de deux contraintes est formée en utilisant l'opérateur && dans l'expression de contrainte :

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>;

Une conjonction de deux contraintes est satisfaite seulement si les deux contraintes sont satisfaites. Les conjonctions sont évaluées de gauche à droite et court-circuitées (si la contrainte de gauche n'est pas satisfaite, la substitution d'argument de template dans la contrainte de droite n'est pas tentée : cela empêche les échecs dus à la substitution en dehors du contexte immédiat).

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, appelle #2. Lors de la vérification des contraintes de #1,
            // 'sizeof(char) > 1' n'est pas satisfait, donc get_value<T>() n'est pas vérifié
}

Disjonctions

La disjonction de deux contraintes est formée en utilisant l'opérateur || dans l'expression de contrainte.

Une disjonction de deux contraintes est satisfaite si l'une ou l'autre contrainte est satisfaite. Les disjonctions sont évaluées de gauche à droite et court-circuitées (si la contrainte de gauche est satisfaite, la substitution d'argument de template dans la contrainte de droite n'est pas tentée).

template<class T = void>
    requires EqualityComparable<T> || Same<T, void>
struct equal_to;
**Note:** Le code C++ n'a pas été traduit conformément aux instructions, car il se trouve dans des balises `
` et contient des termes spécifiques au C++ qui doivent être préservés. Seul le texte environnant (s'il y en avait) aurait été traduit en français.

Contraintes atomiques

Une contrainte atomique consiste en une expression E et une correspondance des paramètres de template qui apparaissent dans E vers des arguments de template impliquant les paramètres de template de l'entité contrainte, appelée son mappage de paramètres  .

Les contraintes atomiques sont formées durant la normalisation des contraintes . E n'est jamais une expression AND logique ou OR logique (celles-ci forment respectivement des conjonctions et des disjonctions).

La satisfaction d'une contrainte atomique est vérifiée en substituant le mappage des paramètres et les arguments template dans l'expression E . Si la substitution résulte en un type ou une expression invalide, la contrainte n'est pas satisfaite. Sinon, E , après toute conversion lvalue-vers-rvalue, doit être une expression constante prvalue de type bool , et la contrainte est satisfaite si et seulement si elle s'évalue à true .

Le type de E après substitution doit être exactement bool . Aucune conversion n'est permise :

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); // erreur : S<int>{} n'a pas le type bool lors de la vérification de #1,
          // même si #2 est une meilleure correspondance
}

Deux contraintes atomiques sont considérées comme identiques si elles sont formées à partir de la même expression au niveau source et que leurs mappages de paramètres sont équivalents.

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); // erreur, ambigu :
           // le is_meowable<T> dans Meowable et BadMeowableCat forme des contraintes atomiques
           // distinctes qui ne sont pas identiques (et donc ne se subsument pas mutuellement)
    f2(0); // OK, appelle #4, plus contrainte que #3
           // GoodMeowableCat a obtenu son is_meowable<T> de Meowable
}

Contraintes de pliage étendues

Une contrainte de pliage étendue est formée à partir d'une contrainte C et d'un opérateur de pliage (soit && ou || ). Une contrainte de pliage étendue est une expansion de paquet .

Soit N le nombre d'éléments dans les paramètres d'expansion de paquet :

  • Si l'expansion de paquet est invalide (comme l'expansion de paquets de taille différente), la contrainte de pliage étendue n'est pas satisfaite.
  • Si N vaut 0 , la contrainte de pliage étendue est satisfaite si l'opérateur de pliage est && , ou non satisfaite si l'opérateur de pliage est || .
  • Pour une contrainte de pliage étendue avec un N positif, pour chaque i dans [ 1 , N ] , chaque paramètre d'expansion de paquet est remplacé par le i -ème élément correspondant dans l'ordre croissant :
  • Pour les contraintes de pliage étendues dont l'opérateur de pliage est && , si le remplacement du j -ème élément viole C , la contrainte de pliage étendue n'est pas satisfaite. Dans ce cas, aucune substitution n'a lieu pour les i supérieurs à j . Sinon, la contrainte de pliage étendue est satisfaite.
  • Pour les contraintes de pliage étendues dont l'opérateur de pliage est || , si le remplacement du j -ème élément satisfait C , la contrainte de pliage étendue est satisfaite. Dans ce cas, aucune substitution n'a lieu pour les i supérieurs à j . Sinon, la contrainte de pliage étendue n'est pas satisfaite.


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>;
// en C++23, ces deux surcharges de g() ont des contraintes atomiques distinctes
// qui ne sont pas identiques et ne se subsument donc pas mutuellement : les appels à g() sont ambigus
// en C++26, les pliages sont étendus et la contrainte sur la surcharge #2 (mouvement et copie
// requis), subsume la contrainte sur la surcharge #1 (seul le mouvement est requis)
template <class... T>
requires (A<T> && ...) void g(T...); // #1
template <class... T>
requires (C<T> && ...) void g(T...); // #2


(depuis C++26)

Normalisation des contraintes

Normalisation de contrainte est le processus qui transforme une expression de contrainte en une séquence de conjonctions et de disjonctions de contraintes atomiques. La forme normale d'une expression est définie comme suit :

  • La forme normale d'une expression ( E ) est la forme normale de E .
  • La forme normale d'une expression E1 && E2 est la conjonction des formes normales de E1 et E2 .
  • La forme normale d'une expression E1 || E2 est la disjonction des formes normales de E1 et E2 .
  • La forme normale d'une expression C < A1, A2, ... , AN > , où C nomme un concept, est la forme normale de l'expression de contrainte de C , après substitution de A1 , A2 , ... , AN aux paramètres templates respectifs de C dans les mappages de paramètres de chaque contrainte atomique de C . Si une telle substitution dans les mappages de paramètres résulte en un type ou une expression invalide, le programme est mal formé, aucun diagnostic requis.
template<typename T>
concept A = T::value || true;
template<typename U>
concept B = A<U*>; // OK : normalisé à la disjonction de
                   // - T::value (avec le mappage T -> U*) et
                   // - true (avec un mappage vide).
                   // Aucun type invalide dans le mappage même si
                   // T::value est mal formé pour tous les types pointeur
template<typename V>
concept C = B<V&>; // Normalisé à la disjonction de
                   // - T::value (avec le mappage T-> V&*) et
                   // - true (avec un mappage vide).
                   // Type invalide V&* formé dans le mappage => mal formé NDR
  • La forme normale des expressions ( E && ... ) et ( ... && E ) est une contrainte de repli développée, où C est la forme normale de E et l'opérateur de repli est && .
  • La forme normale des expressions ( E || ... ) et ( ... || E ) est une contrainte de repli développée, où C est la forme normale de E et l'opérateur de repli est || .
  • Les formes normales des expressions ( E1 && ... && E2 ) et ( E1 || ... || E2 ) sont les formes normales de
  • ( E1 && ... ) && E2 et ( E1 || ... ) || E2 respectivement, si E1 contient un paquet non développé, ou
  • E1 && ( ... && E2 ) et E1 || ( ... || E2 ) respectivement sinon.
(depuis C++26)
  • La forme normale de toute autre expression E est la contrainte atomique dont l'expression est E et dont le mappage de paramètres est le mappage identité. Cela inclut toutes les expressions de repli , même celles repliant sur les opérateurs && ou || .

Les surcharges définies par l'utilisateur de && ou || n'ont aucun effet sur la normalisation des contraintes.

requires clauses

Le mot-clé requires est utilisé pour introduire une requires clause  , qui spécifie des contraintes sur les arguments de template ou sur une déclaration de fonction.

template<typename T>
void f(T&&) requires Eq<T>; // peut apparaître comme dernier élément d'un déclarateur de fonction
template<typename T> requires Addable<T> // ou juste après une liste de paramètres de template
T add(T a, T b) { return a + b; }

Dans ce cas, le mot-clé requires doit être suivi d'une expression constante (il est donc possible d'écrire requires true ), mais l'intention est qu'un concept nommé (comme dans l'exemple ci-dessus) ou une conjonction/disjonction de concepts nommés ou une expression requires soit utilisée.

L'expression doit avoir l'une des formes suivantes :

  • Une expression primaire , par exemple Swappable < T > , std:: is_integral < T > :: value , ( std:: is_object_v < Args > && ... ) , ou toute expression entre parenthèses.
  • Une séquence d'expressions primaires jointes par l'opérateur && .
  • Une séquence des expressions précédentes jointes par l'opérateur || .
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>(); // erreur, is_purrable<T>() n'est pas une expression primaire
template<class T>
void h(T) requires (is_purrable<T>()); // OK

Ordonnancement partiel des contraintes

Avant toute analyse supplémentaire, les contraintes sont normalisées en substituant le corps de chaque concept nommé et chaque expression requires jusqu'à ce qu'il ne reste qu'une séquence de conjonctions et de disjonctions sur des contraintes atomiques.

Une contrainte P est dite subsumer la contrainte Q s'il peut être prouvé que P implique Q compte tenu de l'identité des contraintes atomiques dans P et Q. (Les types et expressions ne sont pas analysés pour l'équivalence : N > 0 ne subsume pas N >= 0 ).

Plus précisément, d'abord P est converti en forme normale disjonctive et Q est converti en forme normale conjonctive. P subsume Q si et seulement si :

  • chaque clause disjonctive dans la forme normale disjonctive de P subsume chaque clause conjonctive dans la forme normale conjonctive de Q , où
  • une clause disjonctive subsume une clause conjonctive si et seulement s'il existe une contrainte atomique U dans la clause disjonctive et une contrainte atomique V dans la clause conjonctive telles que U subsume V ;
  • une contrainte atomique A subsume une contrainte atomique B si et seulement si elles sont identiques selon les règles décrites ci-dessus .
  • Une contrainte de repli développée A subsume une autre contrainte de repli développée B si elles ont le même opérateur de repli, la contrainte C de A subsume celle de B , et les deux C contiennent un pack non développé équivalent.
(depuis C++26)

La relation de subsomption définit un ordre partiel des contraintes, qui est utilisé pour déterminer :

Si les déclarations D1 et D2 sont contraintes et que les contraintes associées de D1 subsument les contraintes associées de D2 (ou si D2 est non contrainte), alors D1 est dite au moins aussi contrainte que D2 . Si D1 est au moins aussi contrainte que D2 , et que D2 n'est pas au moins aussi contrainte que D1 , alors D1 est plus contrainte que D2 .

Si toutes les conditions suivantes sont satisfaites, une fonction non-template F1 est plus contrainte par l'ordre partiel qu'une fonction non-template F2 :

  • Ils ont la même liste de types de paramètres , en omettant les types des paramètres objet explicites (depuis C++23) .
  • S'il s'agit de fonctions membres, les deux sont des membres directs de la même classe.
  • Si les deux sont des fonctions membres non statiques, elles ont les mêmes types pour leurs paramètres objet.
  • F1 est plus contrainte que F2 .
template<typename T>
concept Decrementable = requires(T t) { --t; };
template<typename T>
concept RevIterator = Decrementable<T> && requires(T t) { *t; };
// RevIterator englobe Decrementable, mais pas l'inverse
template<Decrementable T>
void f(T); // #1
template<RevIterator T>
void f(T); // #2, plus contrainte que #1
f(0);       // int satisfait seulement Decrementable, sélectionne #1
f((int*)0); // int* satisfait les deux contraintes, sélectionne #2 comme plus contrainte
template<class T>
void g(T); // #3 (sans contrainte)
template<Decrementable T>
void g(T); // #4
g(true); // bool ne satisfait pas Decrementable, sélectionne #3
g(0);    // int satisfait Decrementable, sélectionne #4 car plus contrainte
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); // ambigu

Notes

Macro de test de fonctionnalité Valeur Std Fonctionnalité
__cpp_concepts 201907L (C++20) Contraintes
202002L (C++20) Fonctions membres spéciales conditionnellement triviales

Mots-clés

concept , requires , typename

Rapports de défauts

Les rapports de défauts modifiant le comportement suivants ont été appliqués rétroactivement aux normes C++ précédemment publiées.

DR Appliqué à Comportement publié Comportement corrigé
CWG 2428 C++20 ne pouvait pas appliquer des attributs aux concepts autorisé

Voir aussi

Expression requires (C++20) produit une expression prvalue de type bool qui décrit les contraintes