I concept di C++20
I template offrono un potere enorme, ma presentano un difetto storico: quando si passa a un template un tipo che non supporta le operazioni richieste, il compilatore produce messaggi di errore lunghi e criptici, spesso relativi a punti interni del codice lontani dalla causa reale. Lo standard C++20 introduce i concept per risolvere proprio questo problema, permettendo di esprimere in modo chiaro e verificabile i requisiti che un tipo deve soddisfare per essere usato con un template.
Il problema dei template non vincolati
Un template tradizionale accetta qualsiasi tipo, e i requisiti su di esso sono impliciti: emergono solo quando il compilatore, tentando di generare il codice, incontra un'operazione non supportata. Un concept trasforma questi requisiti impliciti in vincoli espliciti, controllati al momento della chiamata. Se un tipo non soddisfa il vincolo, l'errore è immediato e comprensibile, e indica chiaramente quale requisito è stato violato.
Vincolare un template
La libreria standard mette a disposizione una serie di concept predefiniti nell'intestazione concepts. Uno dei più semplici è std::integral, che è soddisfatto solo dai tipi interi. Per vincolare un template basta sostituire la parola chiave typename con il nome del concept desiderato.
#include <concepts>
#include <iostream>
// Accetta solo tipi interi
template <std::integral T>
T doubleValue(T value) {
return value * 2;
}
int main() {
std::cout << doubleValue(21) << std::endl; // Corretto
// doubleValue(3.14) verrebbe rifiutato in modo esplicito
return 0;
}
La clausola requires
Un modo alternativo e più flessibile di applicare i vincoli è la clausola requires, che si colloca dopo l'elenco dei parametri del template. Consente di combinare più concept con operatori logici e di esprimere condizioni articolate. La forma con la clausola è particolarmente utile quando un template ha più parametri con vincoli distinti.
#include <concepts>
// Vincolo espresso tramite clausola requires
template <typename T>
requires std::floating_point<T>
T half(T value) {
return value / 2;
}
Definire un concept personalizzato
Oltre a usare i concept della libreria standard, è possibile definirne di propri per catturare i requisiti specifici del proprio dominio. Un concept si dichiara con la parola chiave concept e viene definito da un'espressione booleana valutata in fase di compilazione. Al suo interno, un blocco requires permette di elencare le espressioni che devono essere valide per il tipo in esame.
#include <concepts>
// Un tipo e Addable se supporta l'operatore di somma
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
// Il template accetta solo tipi che soddisfano Addable
template <Addable T>
T sum(T a, T b) {
return a + b;
}
Nel blocco requires, l'espressione tra parentesi graffe seguita dalla freccia impone che l'operazione sia valida e che il suo risultato abbia un tipo compatibile con quello richiesto. Il concept std::same_as verifica proprio l'uguaglianza tra due tipi.
Vincoli su più requisiti
I concept si compongono con gli operatori logici, permettendo di esprimere requisiti multipli in modo naturale. Un tipo può essere richiesto, ad esempio, contemporaneamente intero e confrontabile, e la combinazione risulta leggibile quanto una normale espressione booleana.
#include <concepts>
// Richiede un tipo intero e regolare
template <typename T>
concept Countable = std::integral<T> && std::regular<T>;
I vantaggi dei concept
I concept portano tre benefici principali. Rendono i messaggi di errore chiari e immediati, indicando con precisione quale requisito non è soddisfatto anziché sommergere il programmatore in dettagli interni. Documentano l'interfaccia dei template, poiché il nome del concept comunica a colpo d'occhio cosa ci si aspetta dal tipo. Infine, permettono al compilatore di scegliere tra più versioni di una funzione in base ai vincoli soddisfatti, un meccanismo che sostituisce in modo elegante tecniche più oscure del passato. I concept rappresentano una delle aggiunte più significative del C++ moderno e rendono la programmazione generica accessibile e sicura come non era mai stata.