Prefazione
Questo post vuole essere una piccola esercitazione introduttiva ai concepts
, una nuova caratteristica di C++20 (e disponibile anche in parte nelle versioni precedenti di GCC).
In breve, introdurremo la terminologia utilizzata nel contesto dei concepts e come utilizzare quest’ultimi a nostro vantaggio.
Per avere informazioni più esaustive vi lascio subito la documentazione, piuttosto densa, su cppreference.
Motivazione
La prima domanda che balza subito all’occhio è “a cosa diavolo potranno mai servire i concepts?”.
Quando scriviamo del codice vogliamo, quasi sempre, che gli algoritmi e le strutture dati che implementiamo siano generiche, ossia che possano essere utilizzati per tipi di dato diversi. Quindi un’unica soluzione generica e senza doverla reimplementare per tipi di dato particolari. Questo ci serve per ottenere molteplici vantaggi:
- Maggiore manutenibilità grazie all’indirezione
- Riutilizzare il codice per altri scopi
- Fornire all’utente la possibilità di fornire dei tipi di dato propri.
L’esempio più banale che posso fornirvi sono gli algoritmi generici e le strutture dati della libreria stl (std::swap, std::vector).
L’astrazione che si crea può essere riassunta nella parola modello
. Un modello è, per definizione, qualcosa che può essere istanziato con un tipo di dato qualsiasi.
Tuttavia, può essere necessario applicare una restrizione, qualora il tipo di dato non possa essere scelto arbitrariamente, magari perchè il modello si aspetta una particolare interfaccia dal tipo di dato.
Supponiamo di avere un algoritmo che dati due dati dello stesso tipo voglia effettuare l’operazione di addizione binaria e fornire il risultato.
|
|
Il parametro generico T del template non è vincolato, da un punti di vista della compilazione, questo può essere istanziato con qualsiasi tipo di dato. Tuttavia, si può facilmente notare che, se si richiedere l’istanziazione di questa funzione per certi tipi di dato, nel momento della compilazione, il compilatore genererebbe un errore.
Questo avviene perchè il template della funzione in questione richiede implicitamente che i tipi di dato passati come parametro debbano fornire l’operatore binario di addizione. Quindi passando un tipo di dato che non presenta l’operatore di addizione binaria, il compilatore riscontra l’impossibilità di generare la funzione.
Ma in che punto il compilatore fallisce?
Si potrebbe pensare che sia nel punto in cui viene dichiarato il template, ebbene no, l’errore di compilazione evidenziato è nel punto esatto in cui tale operatore viene utilizzato.
Questro comporta messaggi di errore complessi da decifrare che causano grave frustazione all’utente. In basi di codice di dimensioni medio-grandi con strutture dati annidate la situazione peggiora esponenzialmente.
Per risolvere questo inconveniente dobbiamo introdurre dei vincoli
per definire in maniera esplicita i requisiti dei paremetri del template
Terminologia
Modello (template)
Un modello è un costrutto che genera un tipo o una funzione normale in fase di compilazione in base agli argomenti forniti dall’utente per i parametri del modello. Gli argomenti di un modello possono essere vincolati.
Requisiti
Sono racchiusi dietro la parola chiave requires
che restituisce una risulato booleano che descrive il rispetto o meno dei requisiti.
Siccome non voglio dilungarmi troppo vi lascio il link alla documentazione.
Vincolo (constraint)
Un vincolo
è un insieme di requisiti
sugli argomenti di un modello.
Questi sono usati per:
- Selezionare correttamente gli overloading delle funzioni.
- Decidere la specializzazione più appropriata per un modello.
Concetti (concepts)
Un concetto è un predicato che racchiude un insieme di vincoli. Ogni concetto, viene valutato in fase di compilazione e dventa parte dell’interfaccia di un modello in cui viene usato sottoforma di vincolo.
Inoltre abbiamo che:
- Un tipo di dato che soddisfa tutti i requisiti (e quindi i vincoli) di un concetto si dice che modella tale concetto.
- Un concetto che è composto da un altro concetto e da vincoli aggiuntivi si dice che rifinisce il concetto (o i concetti).
Sintassi
A seconda della complessità delle dichiarazioni di un vincolo, si può usufruire di tre diverse sintassi per imporre dei vincoli. Tutte le definizioni di seguito sono equivalenti ed è possibile combinarle insieme. Si tenga presente che std::integral è un concetto predefinito.
Diciarazione completa ed esplicita
Molto utile se si devono imporre vincoli multipli.
|
|
Dichiarazione intermedia
|
|
Dichiarazione chiara ed implicita
|
|
Soluzione
Per risolvere il nostro problema, andremo a dichiarare il concept Addable, e lo applicheremo come vincolo agli argomenti del modello della funzione add. In questo caso ho deciso di implementare il vincolo in maniera implicita.
|
|
Dal codice precedente si evince che il modello rifiuta completamente e immediatamente (compilazione) qualsiasi tipo di dato che non soddisfi il requisito specificato. A differenza però della soluzione precedente il compilatore mostrerà un errore più comprensibile.
Conclusione
In questo post abbiam visto come utilizzare i concepts e i vincoli per rendere i nostri template delle interfaccie più sicure da utilizzare e meno prone a errori di difficile comprensione per l’utente. Per approfondire meglio il tema e per imarare meglio a costruire i propri concept vi lascio alla documentazione di cppreference sui vincoli e concetti.