Concept e vincoli di C++20
Definisci requisiti precisi sugli argomenti template usando i concept. Scrivi codice generico più pulito con le clausole requires, i template di funzione abbreviati e i concept della libreria standard.
Il problema che i concept risolvono
Prima dei concept, i messaggi di errore dei template erano criptici — usare std::sort su un tipo privo di operator< poteva produrre pagine di errori provenienti dalle viscere dell'implementazione della libreria standard. I concept risolvono il problema permettendoti di dichiarare i requisiti fin dall'inizio sui parametri template.
Un concept è un predicato booleano con un nome, valutato a tempo di compilazione. Verifica se un tipo soddisfa un insieme di requisiti sintattici e semantici. Quando un concept non è soddisfatto, il compilatore produce un messaggio di errore chiaro e conciso che indica il requisito non soddisfatto — non un muro di backtrace di istanziazioni template.
I concept sono stati introdotti in C++20 e rappresentano il miglioramento più significativo della programmazione generica dai template stessi in poi.
Definire i concept
Un concept si definisce con la parola chiave concept seguita da un'espressione requires che specifica quali operazioni un tipo deve supportare. L'espressione requires può verificare la validità di espressioni, i tipi di ritorno, i tipi annidati e altro ancora.
#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
}Clausole requires e sintassi abbreviata
Ci sono diversi modi di applicare un concept a un template. La clausola requires va dopo la lista dei parametri template oppure dopo la firma della funzione. La sintassi abbreviata dei template di funzione usa Concept auto come tipo del parametro ed è la forma più concisa.
#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
}Composizione dei concept e concept standard
I concept si compongono naturalmente con && (congiunzione) e || (disgiunzione). La libreria standard mette a disposizione un ricco insieme di concept in :
Concept fondamentali del linguaggio:
- std::same_as — T e U sono lo stesso tipo
- std::derived_from — Derived deriva da Base
- std::convertible_to — From è convertibile implicitamente in To
- std::integral, std::floating_point, std::signed_integral
Concept di confronto:
- std::equality_comparable — supporta == e !=
- std::totally_ordered — supporta <, >, <=, >=, ==, !=
Concept sugli oggetti:
- std::movable, std::copyable, std::semiregular, std::regular
Concept sui callable:
- std::invocable — F può essere chiamato con Args
- std::predicate — F è un predicato che restituisce bool
Concept sugli iteratori ():
- std::input_iterator, std::forward_iterator, std::random_access_iterator, std::contiguous_iterator
Concept sui range ():
- std::ranges::range, std::ranges::sized_range, std::ranges::random_access_range
Subsunzione dei concept e risoluzione degli overload
Quando più overload vincolati corrispondono, il compilatore usa la subsunzione dei concept per scegliere il più specifico. Un concept A subsume un concept B se i vincoli di A implicano logicamente quelli di B. Questo permette il "raffinamento": un concept più specifico vince su uno più generico.
#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
}Il pattern requires requires
Il requires requires, un po' insolito a vedersi, compare quando usi un'espressione requires estemporanea direttamente in una clausola requires, senza definire un concept con un nome. Il primo requires introduce la clausola di vincolo; il secondo requires dà inizio all'espressione 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
}Quando progetti un concept:
- Dai al concept il nome di ciò che il tipo È, non di ciò che serve alla funzione — usa Sortable, non HasLessThan. I concept dovrebbero catturare categorie semantiche.
- Preferisci i concept della libreria standard — prima di scriverne uno tuo, controlla se , o ha già quello che ti serve.
- Mantieni i concept piccoli e componibili — combina concept piccoli con && invece di creare requisiti monolitici.
- Preferisci i concept con un nome al requires requires estemporaneo — la forma con nome è più leggibile e riutilizzabile.
- Documenta i requisiti semantici nei commenti — i concept verificano la sintassi, non la semantica. Se pretendi che operator+ sia commutativo, scrivilo in un commento.
- I concept sono predicati booleani con un nome che vincolano i parametri template e producono messaggi di errore chiari
- L'espressione
requiresverifica la validità di espressioni, i tipi di ritorno e i requisiti annidati - Quattro modi per applicare i concept: vincolo sul parametro template, clausola requires, requires posticipato o
Concept auto - I concept si compongono con
&&e||; la subsunzione seleziona l'overload più vincolato - La libreria standard fornisce concept in
,e requires requiresè un vincolo inline estemporaneo — per leggibilità preferisci i concept con un nome
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. Cosa significa `void print(std::integral auto value)`?
2. Se il concept A è definito come `concept A = B<T> && C<T>`, cosa succede quando corrispondono sia un overload vincolato da B sia uno vincolato da A?
3. Perché in `requires requires(T a) { a.size(); }` la parola chiave 'requires' compare due volte?