Concepts et contraintes en C++20
Définissez des exigences précises sur les arguments de template grâce aux concepts. Écrivez du code générique plus propre avec les clauses requires, les templates de fonctions abrégés et les concepts de la bibliothèque standard.
Le problème que résolvent les concepts
Avant les concepts, les messages d'erreur des templates étaient cryptiques — utiliser std::sort sur un type dépourvu d'operator< pouvait produire des pages d'erreurs au plus profond de l'implémentation de la bibliothèque standard. Les concepts résolvent ce problème en vous permettant d'énoncer les exigences dès le départ sur les paramètres de template.
Un concept est un prédicat booléen nommé, évalué à la compilation. Il vérifie si un type satisfait un ensemble d'exigences syntaxiques et sémantiques. Lorsqu'un concept n'est pas satisfait, le compilateur produit un message d'erreur clair et concis qui pointe vers l'exigence non satisfaite — et non un mur de traces d'instanciation de templates.
Les concepts ont été introduits en C++20 et représentent l'amélioration la plus importante de la programmation générique depuis les templates eux-mêmes.
Définir des concepts
Un concept se définit avec le mot-clé concept suivi d'une expression requires qui précise les opérations qu'un type doit prendre en charge. L'expression requires peut vérifier la validité d'expressions, les types de retour, les types imbriqués, et plus encore.
#include <concepts>
#include <iostream>
#include <string>
// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a+b must return something convertible to T
{ a == b } -> std::convertible_to<bool>; // must be comparable
requires std::copy_constructible<T>; // nested requirement
};
// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.empty() } -> std::convertible_to<bool>;
};
// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
return a + b + c;
}
template <Sizable Container>
void report_size(const Container& c) {
std::cout << "Size: " << c.size()
<< (c.empty() ? " (empty)" : "") << '\n';
}
int main() {
std::cout << sum_three(1, 2, 3) << '\n'; // 6
std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0
std::string s = "hello";
report_size(s); // Size: 5
std::vector<int> v{1, 2, 3};
report_size(v); // Size: 3
}Clauses requires et syntaxe abrégée
Il existe plusieurs façons d'appliquer un concept à un template. La clause requires se place après la liste des paramètres de template ou après la signature de la fonction. La syntaxe abrégée des templates de fonctions utilise Concept auto comme type de paramètre, ce qui constitue la forme la plus concise.
#include <concepts>
#include <iostream>
// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
while (b != 0) {
T temp = b;
b = a % b;
a = temp;
}
return a;
}
// Method 2: requires clause after template parameters
template <typename T>
requires std::integral<T>
T lcm(T a, T b) {
return (a / gcd(a, b)) * b;
}
// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
return value < 0 ? -value : value;
}
// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
std::cout << "Integral: " << value << '\n';
}
// Concept auto works for return types too (C++20)
std::integral auto compute() {
return 42; // must return an integral type
}
int main() {
std::cout << gcd(12, 8) << '\n'; // 4
std::cout << lcm(12, 8) << '\n'; // 24
std::cout << absolute(-42) << '\n'; // 42
print_integral(100); // Integral: 100
// print_integral(3.14); // ERROR: constraint not satisfied
}Composition de concepts et concepts standard
Les concepts se composent naturellement avec && (conjonction) et || (disjonction). La bibliothèque standard fournit un riche ensemble de concepts dans :
Concepts du langage de base :
- std::same_as — T et U sont le même type
- std::derived_from — Derived dérive de Base
- std::convertible_to — From est implicitement convertible en To
- std::integral, std::floating_point, std::signed_integral
Concepts de comparaison :
- std::equality_comparable — prend en charge == et !=
- std::totally_ordered — prend en charge <, >, <=, >=, ==, !=
Concepts d'objets :
- std::movable, std::copyable, std::semiregular, std::regular
Concepts d'appelables :
- std::invocable — F peut être appelé avec Args
- std::predicate — F est un prédicat renvoyant un bool
Concepts d'itérateurs () :
- std::input_iterator, std::forward_iterator, std::random_access_iterator, std::contiguous_iterator
Concepts de ranges () :
- std::ranges::range, std::ranges::sized_range, std::ranges::random_access_range
Subsomption de concepts et résolution de surcharge
Lorsque plusieurs surcharges contraintes correspondent, le compilateur utilise la subsomption de concepts pour choisir la plus spécifique. Un concept A subsume un concept B si les contraintes de A impliquent logiquement celles de B. Cela permet le « raffinement » : un concept plus spécifique l'emporte sur un concept plus général.
#include <concepts>
#include <iostream>
// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
{ os << t };
};
template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;
// Less constrained overload
template <Printable T>
void display(const T& value) {
std::cout << "[generic] " << value << '\n';
}
// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
std::cout << "[number] " << value << '\n';
}
int main() {
display(42); // [number] 42 — PrintableNumber is more specific
display(std::string("hi")); // [generic] hi — only Printable matches
display(3.14); // [generic] 3.14 — not integral
}Le motif requires requires
La construction quelque peu inhabituelle requires requires apparaît lorsque vous utilisez une expression requires ad hoc directement dans une clause requires, sans définir de concept nommé. Le premier requires introduit la clause de contrainte ; le second requires commence l'expression requires.
#include <iostream>
#include <string>
// Ad-hoc constraint without naming a concept
template <typename T>
requires requires(T a, T b) {
{ a + b } -> std::same_as<T>;
}
T add(T a, T b) {
return a + b;
}
// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <AddableToSelf T>
T add_named(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << '\n'; // 3
std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
std::cout << add_named(10, 20) << '\n'; // 30
}Lorsque vous concevez des concepts :
- Nommez les concepts d'après ce qu'EST le type, pas d'après ce dont la fonction a besoin — utilisez Sortable et non HasLessThan. Les concepts doivent capturer des catégories sémantiques.
- Privilégiez les concepts de la bibliothèque standard — avant d'écrire les vôtres, vérifiez si , ou proposent déjà ce dont vous avez besoin.
- Gardez des concepts petits et composables — combinez de petits concepts avec && plutôt que de créer des exigences monolithiques.
- Préférez les concepts nommés aux requires requires ad hoc — la forme nommée est plus lisible et réutilisable.
- Documentez les exigences sémantiques dans des commentaires — les concepts vérifient la syntaxe, pas la sémantique. Si vous exigez que operator+ soit commutatif, indiquez-le dans un commentaire.
- Les concepts sont des prédicats booléens nommés qui contraignent les paramètres de template et fournissent des messages d'erreur clairs
- L'expression
requiresvérifie la validité d'expressions, les types de retour et les exigences imbriquées - Quatre façons d'appliquer des concepts : contrainte sur le paramètre de template, clause requires, requires en fin de déclaration, ou
Concept auto - Les concepts se composent avec
&&et||; la subsomption sélectionne la surcharge la plus contrainte - La bibliothèque standard fournit des concepts dans
,et requires requiresest une contrainte ad hoc en ligne — préférez les concepts nommés pour la lisibilité
Quiz — Testez vos connaissances
(20 XP)1. Que signifie `void print(std::integral auto value)` ?
2. Si le concept A est défini par `concept A = B<T> && C<T>`, que se passe-t-il lorsqu'une surcharge contrainte par B et une surcharge contrainte par A correspondent toutes les deux ?
3. Pourquoi `requires requires(T a) { a.size(); }` contient-il deux fois le mot-clé « requires » ?