Constraints and concepts
Cette page décrit une fonctionnalité expérimentale du langage de base. Pour les exigences de type nommées utilisées dans la spécification de la bibliothèque standard, consultez les exigences nommées
Modèles de classe , modèles de fonction , et les fonctions non-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, qui peuvent être utilisées pour sélectionner les surcharges de fonction et les spécialisations de template les plus appropriées.
Les contraintes peuvent également être utilisées pour limiter la déduction automatique de type dans les déclarations de variables et les types de retour de fonction uniquement aux types qui satisfont aux exigences spécifié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 modèle où il est utilisé comme contrainte :
#include <string> #include <locale> using namespace std::literals; // Déclaration du concept "EqualityComparable", qui est satisfait par // tout type T tel que pour les valeurs a et b de type T, // l'expression a==b compile et son résultat est convertible en bool template<typename T> concept bool EqualityComparable = requires(T a, T b) { { a == b } -> bool; }; void f(EqualityComparable&&); // déclaration d'un modèle de fonction contraint // template<typename T> // void f(T&&) requires EqualityComparable<T>; // forme longue équivalente int main() { f("abc"s); // OK, std::string est EqualityComparable f(std::use_facet<std::ctype<char>>(std::locale{})); // Erreur : pas EqualityComparable }
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). Selon la directive centrale 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."
Si la vérification des fonctionnalités est prise en charge, les fonctionnalités décrites ici sont indiquées par la constante macro __cpp_concepts avec une valeur égale ou supérieure à 201507 .
Table des matières |
Espaces réservés
Le placeholder non contraint
auto
et les
placeholders contraints
qui ont la forme
concept-name
<
template-argument-list
(optional)
>
, sont des placeholders pour le type qui doit être déduit.
Les espaces réservés peuvent apparaître dans les déclarations de variables (auquel cas ils sont déduits de l'initialiseur) ou dans les types de retour de fonctions (auquel cas ils sont déduits des instructions return)
std::pair<auto, auto> p2 = std::make_pair(0, 'a'); // premier auto est int, // deuxième auto est char Sortable x = f(y); // le type de x est déduit du type de retour de f, // ne compile que si le type satisfait la contrainte Sortable auto f(Container) -> Sortable; // le type de retour est déduit de l'instruction return // ne compile que si le type satisfait Sortable
Les espaces réservés peuvent également apparaître dans les paramètres, auquel cas ils transforment les déclarations de fonctions en déclarations de modèles (contraintes si l'espace réservé est contraint)
void f(std::pair<auto, EqualityComparable>); // ceci est un modèle avec deux paramètres : // paramètre de type non contraint et paramètre non-type contraint
Les espaces réservés contraints peuvent être utilisés partout où auto peut être utilisé, par exemple, dans les déclarations de lambdas génériques
auto gl = [](Assignable& a, auto* b) { a = *b; };
Si un spécificateur de type contraint désigne un non-type ou un template, mais est utilisé comme un placeholder contraint, le programme est mal formé :
template<size_t N> concept bool Even = (N%2 == 0); struct S1 { int n; }; int Even::* p2 = &S1::n; // erreur, utilisation invalide d'un concept non-type void f(std::array<auto, Even>); // erreur, utilisation invalide d'un concept non-type template<Even N> void f(std::array<auto, N>); // OK
Modèles abrégés
Si un ou plusieurs espaces réservés apparaissent dans une liste de paramètres de fonction, la déclaration de fonction est en réalité une déclaration de modèle de fonction, dont la liste de paramètres de modèle inclut un paramètre inventé pour chaque espace réservé unique, dans l'ordre d'apparition
// forme courte void g1(const EqualityComparable*, Incrementable&); // forme longue : // template<EqualityComparable T, Incrementable U> void g1(const T*, U&); // forme plus longue : // template<typename T, typename U> // void g1(const T*, U&) requires EqualityComparable<T> && Incrementable<U>; void f2(std::vector<auto*>...); // forme longue : template<typename... T> void f2(std::vector<T*>...); void f4(auto (auto::*)(auto)); // forme longue : template<typename T, typename U, typename V> void f4(T (U::*)(V));
Tous les espaces réservés introduits par des spécificateurs de type équivalents contraints ont le même paramètre de template inventé. Cependant, chaque spécificateur non contraint (
auto
) introduit toujours un paramètre de template différent
void f0(Comparable a, Comparable* b); // forme longue : template<Comparable T> void f0(T a, T* b); void f1(auto a, auto* b); // forme longue : template<typename T, typename U> f1(T a, U* b);
Les modèles de fonction et les modèles de classe peuvent tous deux être déclarés en utilisant une
introduction de modèle
, qui a la syntaxe
nom-du-concept
{
liste-de-paramètres
(optionnel)
}
, auquel cas le mot-clé
template
n'est pas nécessaire : chaque paramètre de la
liste-de-paramètres
de l'introduction de modèle devient un paramètre de modèle dont le type (type, non-type, modèle) est déterminé par le type du paramètre correspondant dans le concept nommé.
En plus de déclarer un template, l'introduction de template associe une contrainte de prédicat (voir ci-dessous) qui nomme (pour les concepts de variables) ou invoque (pour les concepts de fonctions) le concept nommé par l'introduction.
EqualityComparable{T} class Foo; // forme longue : template<EqualityComparable T> class Foo; // forme plus longue : template<typename T> requires EqualityComparable<T> class Foo; template<typename T, int N, typename... Xs> concept bool Example = ...; Example{A, B, ...C} struct S1; // forme longue template<class A, int B, class... C> requires Example<A,B,C...> struct S1;
Pour les modèles de fonction, l'introduction de modèle peut être combinée avec des espaces réservés :
Sortable{T} void f(T, auto); // forme longue : template<Sortable T, typename U> void f(T, U); // alternative utilisant uniquement des espaces réservés : void f(Sortable, auto);
|
Cette section est incomplète
Raison : retoucher les pages de déclaration de modèles pour créer un lien vers ici |
Concepts
Un concept est un ensemble nommé d'exigences. La définition d'un concept apparaît au niveau de l'espace de noms et prend la forme d'une fonction template (auquel cas il est appelé concept de fonction ) ou d'une variable template (auquel cas il est appelé concept de variable ). La seule différence est que le mot-clé concept apparaît dans la séquence de spécificateurs de déclaration :
// concept de variable de la bibliothèque standard (Ranges TS) template <class T, class U> concept bool Derived = std::is_base_of<U, T>::value; // concept de fonction de la bibliothèque standard (Ranges TS) template <class T> concept bool EqualityComparable() { return requires(T a, T b) { {a == b} -> Boolean; {a != b} -> Boolean; }; }
Les restrictions suivantes s'appliquent aux concepts de fonction :
-
inlineetconstexprne sont pas autorisés, la fonction est automatiquementinlineetconstexpr -
friendetvirtualne sont pas autorisés -
la spécification d'exception n'est pas autorisée, la fonction est automatiquement
noexcept(true). - ne peut pas être déclarée et définie ultérieurement, ne peut pas être redéclarée
-
le type de retour doit être
bool - la déduction du type de retour n'est pas autorisée
- la liste des paramètres doit être vide
-
le corps de la fonction doit uniquement contenir une instruction
return, dont l'argument doit être une expression de contrainte (contrainte de prédicat, conjonction/disjonction d'autres contraintes, ou une expression requires, voir ci-dessous)
Les restrictions suivantes s'appliquent aux concepts de variable :
-
Doit avoir le type
bool - Ne peut pas être déclaré sans initialiseur
- Ne peut pas être déclaré au niveau de la classe
-
constexprn'est pas autorisé, la variable est automatiquementconstexpr - L'initialiseur doit être une expression de contrainte (contrainte de prédicat, conjonction/disjonction de contraintes, ou une expression requires, voir ci-dessous)
Les concepts ne peuvent pas se référer récursivement à eux-mêmes dans le corps de la fonction ou dans l'initialiseur de la variable :
template<typename T> concept bool F() { return F<typename T::type>(); } // erreur template<typename T> concept bool V = V<T*>; // erreur
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)
Contraintes
Une contrainte est une séquence d'opérations logiques qui spécifie des exigences sur les arguments de template. Elles peuvent apparaître dans des requires-expression s (voir ci-dessous) et directement comme corps de concepts
Il existe 9 types de contraintes :
Les trois premiers types de contraintes peuvent apparaître directement comme corps d'un concept ou comme une clause requires ad hoc :
template<typename T> requires // clause requires (contrainte ad hoc) sizeof(T) > 1 && get_value<T>() // conjonction de deux contraintes de prédicat void f(T);
Lorsque plusieurs contraintes sont attachées à la même déclaration, la contrainte totale est une conjonction dans l'ordre suivant : la contrainte introduite par l'introduction de template , les contraintes pour chaque paramètre de template dans l'ordre d'apparition, la clause requires après la liste des paramètres de template, les contraintes pour chaque paramètre de fonction dans l'ordre d'apparition, la clause requires finale :
// les déclarations déclarent la même fonction template contrainte // avec la contrainte Incrementable<T> && Decrementable<T> template<Incrementable T> void f(T) requires Decrementable<T>; template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // ok // les deux déclarations suivantes ont des contraintes différentes : // la première déclaration a Incrementable<T> && Decrementable<T> // la deuxième déclaration a Decrementable<T> && Incrementable<T> // Même si elles sont logiquement équivalentes. // La deuxième déclaration est mal formée, aucun diagnostic requis template<Incrementable T> requires Decrementable<T> void g(); template<Decrementable T> requires Incrementable<T> void g(); // erreur
Conjonctions
Conjonction de contraintes
P
et
Q
est spécifiée comme
P
&&
Q
.
// exemples de concepts de la bibliothèque standard (Ranges TS) template <class T> concept bool Integral = std::is_integral<T>::value; template <class T> concept bool SignedIntegral = Integral<T> && std::is_signed<T>::value; template <class T> concept bool UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
Une conjonction de deux contraintes est satisfaite uniquement si les deux contraintes sont satisfaites. Les conjonctions sont évaluées de gauche à droite et court-circuitées (si la contrainte gauche n'est pas satisfaite, la substitution d'argument template dans la contrainte droite n'est pas tentée : cela empêche les échecs dus à la substitution en dehors du contexte immédiat). Les surcharges définies par l'utilisateur de
operator&&
ne sont pas autorisées dans les conjonctions de contraintes.
Disjonctions
Disjonction de contraintes
P
et
Q
est spécifiée comme
P
||
Q
.
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 déduction d'argument de template dans la contrainte de droite n'est pas tentée). Les surcharges définies par l'utilisateur de
operator||
ne sont pas autorisées dans les disjonctions de contraintes.
// exemple de contrainte de la bibliothèque standard (Ranges TS) template <class T = void> requires EqualityComparable<T>() || Same<T, void> struct equal_to;
Contraintes de prédicat
Une contrainte de prédicat est une expression constante de type bool . Elle est satisfaite uniquement si elle s'évalue à true
template<typename T> concept bool Size32 = sizeof(T) == 4;
Les contraintes de prédicat peuvent spécifier des exigences sur les paramètres de modèle non typés et sur les arguments de modèle de modèle.
Les contraintes de prédicat doivent s'évaluer directement en bool , aucune conversion n'est autorisée :
template<typename T> struct S { constexpr explicit operator bool() const { return true; } }; template<typename T> requires S<T>{} // contrainte de prédicat incorrecte : S<T>{} n'est pas bool void f(T); f(0); // erreur : contrainte jamais satisfaite
Exigences
Le mot-clé requires est utilisé de deux manières :
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; }
true
si le concept correspondant est satisfait, et false sinon :
template<typename T> concept bool Addable = requires (T x) { x + x; }; // requires-expression template<typename T> requires Addable<T> // requires-clause, not requires-expression T add(T a, T b) { return a + b; } template<typename T> requires requires (T x) { x + x; } // ad-hoc constraint, note keyword used twice T add(T a, T b) { return a + b; }
La syntaxe de l' expression requires est la suivante :
requires
(
liste-de-paramètres
(optionnel)
)
{
séquence-d'exigences
}
|
|||||||||
| parameter-list | - |
une liste séparée par des virgules de paramètres comme dans une déclaration de fonction, sauf que les arguments par défaut ne sont pas autorisés et le dernier paramètre ne peut pas être une ellipse. Ces paramètres n'ont pas de stockage, de liaison ou de durée de vie. Ces paramètres sont dans la portée jusqu'à la fermeture
}
du
requirement-seq
. Si aucun paramètre n'est utilisé, les parenthèses rondes peuvent également être omises
|
| requirement-seq | - | une séquence séparée par des espaces de exigences , décrites ci-dessous (chaque exigence se termine par un point-virgule). Chaque exigence ajoute une autre contrainte à la conjonction de contraintes que cette expression de nécessité définit. |
Chaque exigence dans la requirements-seq est l'une des suivantes :
- exigence simple
- exigences de type
- exigences composées
- exigences imbriquées
Les exigences peuvent faire référence aux paramètres de modèle qui sont dans la portée et aux paramètres locaux introduits dans la liste-de-paramètres . Lorsqu'elle est paramétrée, une expression de contrainte est dite introduire une contrainte paramétrée
La substitution des arguments de template dans une expression requires peut entraîner la formation de types ou d'expressions invalides dans ses exigences. Dans de tels cas,
- Si un échec de substitution se produit dans une expression requires utilisée en dehors d'une entité templatée déclaration, alors le programme est mal formé.
- Si l'expression requires est utilisée dans une déclaration d'une entité templatée , la contrainte correspondante est traitée comme "non satisfaite" et l'échec de substitution n'est pas une erreur , cependant
- Si un échec de substitution se produirait dans une expression requires pour chaque argument de template possible, le programme est mal formé, aucun diagnostic requis :
template<class T> concept bool C = requires { new int[-(int)sizeof(T)]; // invalide pour tout T : mal formé, aucun diagnostic requis };
Exigences simples
Une exigence simple est une instruction d'expression arbitraire. L'exigence est que l'expression soit valide (c'est une contrainte d'expression ). Contrairement aux contraintes de prédicat, l'évaluation n'a pas lieu, seule la correction syntaxique est vérifiée.
template<typename T> concept bool Addable = requires (T a, T b) { a + b; // "l'expression a+b est une expression valide qui sera compilée" }; // exemple de contrainte de la bibliothèque standard (ranges TS) template <class T, class U = T> concept bool Swappable = requires(T&& t, U&& u) { swap(std::forward<T>(t), std::forward<U>(u)); swap(std::forward<U>(u), std::forward<T>(t)); };
Exigences de type
Une exigence de type est le mot-clé typename suivi d'un nom de type, éventuellement qualifié. L'exigence est que le type nommé existe (une contrainte de type ) : cela peut être utilisé pour vérifier qu'un certain type imbriqué nommé existe, ou qu'une spécialisation de modèle de classe désigne un type, ou qu'un modèle d'alias désigne un type.
template<typename T> using Ref = T&; template<typename T> concept bool C = requires { typename T::inner; // nom de membre imbriqué requis typename S<T>; // spécialisation de template de classe requise typename Ref<T>; // substitution de template d'alias requise }; // Exemple de concept de la bibliothèque standard (Ranges TS) template <class T, class U> using CommonType = std::common_type_t<T, U>; template <class T, class U> concept bool Common = requires (T t, U u) { typename CommonType<T, U>; // CommonType<T, U> est valide et désigne un type { CommonType<T, U>{std::forward<T>(t)} }; { CommonType<T, U>{std::forward<U>(u)} }; };
Exigences Composées
Une exigence composée a la forme
{
expression
}
noexcept
(optionnel)
type-retour-final
(optionnel)
;
|
|||||||||
et spécifie une conjonction des contraintes suivantes :
noexcept
est utilisé, l'expression doit également être noexcept (
contrainte d'exception
)
template<typename T> concept bool C2 = requires(T x) { {*x} -> typename T::inner; // l'expression *x doit être valide // ET le type T::inner doit être valide // ET le résultat de *x doit être convertible en T::inner }; // Exemple de concept de la bibliothèque standard (Ranges TS) template <class T, class U> concept bool Same = std::is_same<T,U>::value; template <class B> concept bool Boolean = requires(B b1, B b2) { { bool(b1) }; // la contrainte d'initialisation directe doit utiliser une expression { !b1 } -> bool; // contrainte composée requires Same<decltype(b1 && b2), bool>; // contrainte imbriquée, voir ci-dessous requires Same<decltype(b1 || b2), bool>; };
Exigences imbriquées
Une exigence imbriquée est une autre clause requires , terminée par un point-virgule. Ceci est utilisé pour introduire des contraintes de prédicat (voir ci-dessus) exprimées en termes d'autres concepts nommés appliqués aux paramètres locaux (en dehors d'une clause requires, les contraintes de prédicat ne peuvent pas utiliser de paramètres, et placer une expression directement dans une clause requires en fait une contrainte d'expression ce qui signifie qu'elle n'est pas évaluée)
// exemple de contrainte du Ranges TS template <class T> concept bool Semiregular = DefaultConstructible<T> && CopyConstructible<T> && Destructible<T> && CopyAssignable<T> && requires(T a, size_t n) { requires Same<T*, decltype(&a)>; // imbriquée : "Same<...> évalue à true" { a.~T() } noexcept; // composée : "a.~T()" est une expression valide qui ne lève pas d'exception requires Same<T*, decltype(new T)>; // imbriquée : "Same<...> évalue à true" requires Same<T*, decltype(new T[n])>; // imbriquée { delete new T }; // composée { delete new T[n] }; // composée };
Résolution de concept
Comme toute autre fonction template, un concept de fonction (mais pas un concept de variable) peut être surchargé : plusieurs définitions de concept peuvent être fournies qui utilisent toutes le même concept-name .
La résolution de concept est effectuée lorsqu'un concept-name (qui peut être qualifié) apparaît dans
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 void f(C); // l'ensemble des concepts référencés par C inclut à la fois #1 et #2 ; // la résolution de concept (voir ci-dessous) sélectionne #1.
Afin d'effectuer la résolution de concept, template parameters de chaque concept correspondant au nom (et à la qualification, le cas échéant) est mis en correspondance avec une séquence d' concept arguments , qui sont des arguments de template et des wildcards . Un wildcard peut correspondre à un paramètre de template de tout type (type, non-type, template). L'ensemble d'arguments est construit différemment, selon le contexte
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f1(const C1*); // <wildcard> matches <T>, selects #1
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f2(C1<char>); // <wildcard, char> matches <T, U>, selects #2
template<typename... Ts> concept bool C3 = true; C3{T} void q2(); // OK: <T> matches <...Ts> C3{...Ts} void q1(); // OK: <...Ts> matches <...Ts>
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 template <typename T> void f(T) requires C<T>(); // matches #1
La résolution de concept est effectuée en faisant correspondre chaque argument au paramètre correspondant de chaque concept visible. Les arguments de template par défaut (s'ils sont utilisés) sont instanciés pour chaque paramètre qui ne correspond pas à un argument, puis sont ajoutés à la liste d'arguments. Un paramètre de template correspond à un argument seulement s'il a la même nature (type, non-type, template), sauf si l'argument est un caractère générique. Un paramètre pack correspond à zéro ou plusieurs arguments tant que tous les arguments correspondent au modèle en nature (sauf s'ils sont des caractères génériques).
Si un argument ne correspond pas à son paramètre correspondant ou s'il y a plus d'arguments que de paramètres et que le dernier paramètre n'est pas un pack, le concept n'est pas viable. S'il y a zéro ou plus d'un concept viable, le programme est mal formé.
template<typename T> concept bool C2() { return true; } template<int T> concept bool C2() { return true; } template<C2<0> T> struct S1; // erreur : <wildcard, 0> ne correspond // ni à <typename T> ni à <int T> template<C2 T> struct S2; // les deux #1 et #2 correspondent : erreur
|
Cette section est incomplète
Raison : nécessite un exemple avec des concepts significatifs, pas ces substituts 'return true' |
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, qui sont des contraintes de prédicat, des contraintes d'expression, des contraintes de type, des contraintes de conversion implicite, des contraintes de déduction d'arguments et des contraintes d'exception.
Le concept
P
est dit
subsumer
le concept
Q
s'il peut être prouvé que
P
implique
Q
sans analyser les types et expressions pour l'équivalence (donc
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, puis ils sont comparés comme suit :
-
chaque contrainte atomique
Asubsume la contrainte atomique équivalenteA -
chaque contrainte atomique
Asubsume une disjonctionA||Bet ne subsume pas une conjonctionA&&B -
chaque conjonction
A&&BsubsumeA, mais une disjonctionA||Bne subsume pasA
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 de 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 normalisées de D1 englobent les contraintes normalisées de D2 (ou si D1 est contrainte et 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.
template<typename T> concept bool Decrementable = requires(T t) { --t; }; template<typename T> concept bool RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIterator englobe Decrementable, mais pas l'inverse // RevIterator est plus contraint que Decrementable void f(Decrementable); // #1 void f(RevIterator); // #2 f(0); // int satisfait seulement Decrementable, sélectionne #1 f((int*)0); // int* satisfait les deux contraintes, sélectionne #2 comme plus contraint void g(auto); // #3 (non contraint) void g(Decrementable); // #4 g(true); // bool ne satisfait pas Decrementable, sélectionne #3 g(0); // int satisfait Decrementable, sélectionne #4 car il est plus contraint
Mots-clés
Support du compilateur
GCC >= 6.1 prend en charge cette spécification technique (option requise - fconcepts )