Vai al contenuto

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.

concept_definition.cpp
#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.

requires_clause.cpp
#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.

subsumption.cpp
#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.

requires_requires.cpp
#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
}
Buona pratica

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.

Concetti chiave
  • I concept sono predicati booleani con un nome che vincolano i parametri template e producono messaggi di errore chiari
  • L'espressione requires verifica 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?