Constraints and concepts (since C++20)
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
>
|
|||||||||
| 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
- déclaration de paramètre de modèle de type ,
- spécificateur de type de substitution ,
- exigence composée .
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 :
|
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 :
- 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 ;
- l'expression de contrainte dans la requires clause après la liste des paramètres du template ;
- l'expression de contrainte introduite pour chaque paramètre avec type de substitution contraint dans une déclaration de template de fonction abrégé ;
- 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;
` 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
Soit N le nombre d'éléments dans les paramètres d'expansion de paquet :
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ù
Cnomme un concept, est la forme normale de l'expression de contrainte deC, après substitution deA1,A2, ... ,ANaux paramètres templates respectifs deCdans les mappages de paramètres de chaque contrainte atomique deC. 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
|
(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
Psubsume chaque clause conjonctive dans la forme normale conjonctive deQ, où -
une clause disjonctive subsume une clause conjonctive si et seulement s'il existe une contrainte atomique
Udans la clause disjonctive et une contrainte atomiqueVdans la clause conjonctive telles queUsubsumeV; -
une contrainte atomique
Asubsume une contrainte atomiqueBsi et seulement si elles sont identiques selon les règles décrites ci-dessus .
|
(depuis C++26) |
La relation de subsomption définit un ordre partiel des contraintes, qui est utilisé pour déterminer :
- le meilleur candidat viable pour une fonction non template dans la résolution de surcharge
- l' adresse d'une fonction non template dans un ensemble de surcharge
- la meilleure correspondance pour un argument template template
- ordonnancement partiel des spécialisations de template de classe
- ordonnancement partiel des templates de fonction
|
Cette section est incomplète
Raison : liens retours depuis la section ci-dessus vers ici |
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.
-
F1est plus contrainte queF2.
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
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 |