3.2 “Back to the Future”
3.6. Policy-Based Class Design and Metaprogramming
Un framework ad alte prestazioni per la sintesi audio, come pCM+ vuole essere, così come non può sottrarsi dal prendere vantaggio degli ultimi sviluppi tecnologi nel campo dei processori multi-core, deve anche sfruttare i più recenti risultati nell’ambito della tecniche di progettazione e programma- zione, sia per incrementare ulteriormente le prestazioni, sia per raggiungere alti livelli di flessibilità e affidabilità, minimizzando le responsabilità lasciate al codice utente.
Sono ormai ben noti i vantaggi in termini di flessibilità e di riusabilità della programmazione gene- rica [Järvi05], e non è un caso che librerie sofisticate e ad alte prestazioni come STL, Intel TBB, Loki e la famiglia di librerie Boost, si fondino su tale tecnica di programmazione.
Tra la fine del secolo scorso, e l’inizio del nuovo millennio, gli sviluppi che hanno fatto seguito alla ricerca nell’ambito della programmazione generica hanno dimostrato come il template system del C++ sia turing completo, consentendo di esprimere una qualunque computazione attraverso l’uso dei template. Diversamente dalla programmazione “tradizionale”, tale computazione è svolta a tem- po di compilazione.
Ora, quello che è importante e significativo per noi, è la consapevolezza del fatto che il sistema dei template del C++ costituisce una vera e propria macchina di generazione di codice, sfruttabile per produrre incrementi sul fronte della flessibilità (promuovendo la cooperazione tra il codice utente e il codice interno al framework), dell’affidabilità (promuovendo la valutazione statica degli errori), e delle prestazioni (anticipando il più possibile i calcoli a tempo di compilazione). Infatti, i template producono una più stretta cooperazione tra l’utente e il framework: l’utente controlla letteralmente il modo in cui il codice viene generato, in modo vincolato dal framework stesso. Adottare il poli- morfismo statico, piuttosto che il polimorfismo dinamico12, porta con se vantaggi anche nell’ambito
della gestione degli errori: a causa della natura statica della programmazione generica e della meta- programmazione, gli errori causati da incompatibilità del codice utente rispetto ai vincoli imposti dal framework sono valutati durante la compilazione. Anticipare la valutazione degli errori in fase di compilazione diminuisce (anche drasticamente) la possibilità di produrre errori a tempo di esecu- zione.
L’implementazione di pCM+ combina in modo complementare la programmazione generica con la metaprogrammazione [Abrahams05]. In particolare, è stata adottata la tecnica di programmazione generica nota come policy-based class design (per approfondimenti, si veda [Alexandrescu01], ca-
12 Il C++ supporta due tipi di polimorfismo. Il polimorfismo dinamico consente la gestione di oggetti con tipo di deriva- zione multipla per mezzo di un puntatore o un riferimento ad una singola classe base. Il polimorfismo statico consente ad oggetti di tipi defferenti di essere manipolati nello stesso modo in virtù del loro supporto ad una sintassi comune. Le parole dinamico e statico indicano che il tipo reale dell’oggetto è determinato a runtime o a tempo di compilazione, rispettivamente. Il polimorfismo dinamico, insieme al late-binding (o runtime dispatch, dir si voglia) fornito in C++ dalle funzioni virtuali, è la caratteristica chiave della programmazione object-oriented. Il polimorfismo statico (noto anche come polimorfismo parametrico) è essenziale alla programmazione generica.
pitolo 1). Come supporto alla metaprogrammazione è stato adottata la libreria Boost Metaprogram- ming Library13 (MPL).
La programmazione generica introduce alcuni concetti e termini nuovi, che per chiarezza definiamo brevemente di seguito.
3.6.1. Concetto
Un concetto è un insieme di requisiti su di un tipo. I requisiti possono essere semantici o sintattici. Per esempio, il concetto di “ordinabile” può essere definito come un insieme di requisiti che abili- tano un array ad essere ordinato. Un tipo T dovrebbe essere ordinabile se:
• x < y ritorna un valore booleano, e rappresenta un ordinamento totale sugli elementi di tipo T.
• swap(x,y) scambia gli elementi x ed y.
Definito il concetto di tipo ordinabile, è possibile scrivere una funzione template di ordinamento in C++ che ordina un array di un qualunque tipo che rispetti i vincoli forniti dal concetto di ordinabile. Due approcci per definire i concetti sono le espressioni valide e le pseudo-signature [Siek05]. L’ap- proccio delle pseudo-signature descrive la sintassi attraverso un insieme di dichiarazioni di funzioni (in altre parole, specifica le operazioni del concetto). L’approccio delle espressioni valide descrive la sintassi valida specificando direttamente le espressioni (non le operazioni) che dovrebbero essere supportate. Lo standard ISO C++ segue quest’ultimo approccio, specificando quali sono i pattern (le espressioni) entro cui un concetto può essere usato.
Per descrivere i requisiti che dovrebbero essere osservati dai tipi coinvolti nell’uso del framework pCM+ (ovvero per descrivere i concetti coinvolti in pCM+), adotteremo l’approccio delle pseudo- signature, in quanto permettono di definire un concetto in modo conciso ed immediato.
Per esempio, la seguente tabella mostra le pseudo-signature per un tipo ordinabile T.
Tabella 3.1: Pseudo-signature del concetto “ordinabile”.
Pseudo-Signature Semantica
bool operator<(const T& x, const T& y) Confronta x e y.
void swap(T& x, T& y) Scambia gli elementi x e y.
Una signature reale (cioè, quella effettivamente implementata nel codice) può differire della pseu- do-signature che implementa se la differenza è causata da conversioni implicite. Per esempio, sia U un tipo che implementa la signature reale per l’operatore operator< in tabella 3.1; tale signature reale può essere espressa come int operator<( U x, U y ), in quanto il C++ consente la conver- sione implicita da int a bool, e la conversione implicita da U a (const U&). Analogamente, la signa- ture reale bool operator<( U &x, U &y ) è accettabile, in quanto il C++ consente l’aggiunta impli- cita di un qualificatore const ad un tipo riferimento.
3.6.2. Modello
Un tipo modella un concetto se soddisfa i requisiti del concetto. Per esempio, il tipo int modella il concetto ordinabile in tabella 3.1 se esiste una funzione swap(x,y) che scambia due valori x e y di tipo int. L’altro requisito del concetto ordinabile, ovvero x < y, è già soddisfatto dall’operatore built-in operator< sul tipo int.