2

Il modificatore auto

In teoria, il funzionamento di auto è assolutamente semplice, ma il suo comportamento è molto più raffinato di quanto possa sembrare a prima vista. Utilizzandolo si risparmia tempo, certamente, ma si evitano anche i problemi di correttezza e prestazioni che possono affliggere le dichiarazioni manuali del tipo. Inoltre, alcuni dei risultati delle deduzioni del tipo con auto, anche se sono fedelmente conformi all’algoritmo prescritto, sono, dal punto di vista del programmatore, semplicemente sbagliate. In questo caso, è importante sapere come guidare auto verso la risposta corretta; tornare alle dichiarazioni manuali del tipo è un’alternativa da evitare il più possibile.

Questo breve capitolo descrive tutti i dettagli del modificatore auto.

Elemento 5 – Preferire auto alle dichiarazioni esplicite del tipo

Ah… quella semplice gioia di un:

int x;

Aspetta, dannazione, ho dimenticato di inizializzare x, così il suo valore è indeterminato. Forse. Magari verrà inizializzato a 0. Dipende dal contesto. Oh mio Dio!

Basta preoccuparsi. Ecco la gioia di dichiarare una variabile locale che verrà inizializzata de-referenziando un iteratore:

template<typename It> // algoritmo per dwim ("fai quello che voglio!")
void dwim(It b, It e) // per tutti gli elementi
{ // nell'intervallo fra b ed e
while (b != e) {  
typename std::iterator_traits<It>::value_type  
currValue = *b;  
 
  }
}  
}  
   

Accidenti! “typename std::iterator_traits<It>::value_type” solo per esprimere il tipo di valore puntato da un iteratore? Davvero? A casa mia, la “gioia” è una cosa un po’ diversa. Dannazione. Ma, aspetta, non ne avevamo già parlato?

OK, una semplice gioia (e siamo a tre): la delizia di dichiarare una variabile locale il cui tipo è quello di una closure. OK. Il tipo di una closure è conosciuto solo dal compilatore, pertanto non può essere scritto. Ancora una volta, dannazione!

Dannazione, dannazione e dannazione! La programmazione in C++, allora, non è quell’esperienza gioiosa che dovrebbe essere!

In effetti, un tempo era tutto dannatamente complicato. Ma a partire dal C++11 tutti questi problemi spariscono, grazie alla presenza di auto. Le variabili auto ricevono il proprio tipo tramite deduzione dall’inizializzatore, dunque devono essere inizializzate. Questo significa che potete dire addio a tutti quei problemi legati alla mancata inizializzazione delle variabili, imboccando la grande autostrada del C++ moderno:

int x1; // potenzialmente non inizializzato
auto x2; // errore! Inizializzatore obbligatorio
auto x3 = 0; // bene, il valore di x è ben definito

In ogni autostrada che si rispetti, non si trovano buche, ovvero i problemi legati alla dichiarazione di una variabile locale il cui valore è quello di un iteratore de-referenziato:

template<typename It> // come prima
void dwim(It b, It e)  
{  
while (b != e) {  
auto currValue = *b;  
 
}  
}  

E poiché auto usa la deduzione del tipo (vedi Elemento 2), può rappresentare tipi noti solo al compilatore:

auto derefUPLess = // funzione di confronto
[](const std::unique_ptr<Widget>& p1, // per i Widget
const std::unique_ptr<Widget>& p2) // puntati da
{ return *p1 < *p2; }; // std::unique_ptr

Molto interessante. In C++14, la situazione migliora ulteriormente, poiché anche i parametri delle espressioni lambda possono impiegare auto:

auto derefLess = // funzione di confronto
[](const auto& p1, // C++14 per i
const auto& p2) // valori puntati
{ return *p1 < *p2; }; // da qualsiasi cosa
  // di tipo puntatore

Ciononostante, forse state pensando che non c’è bisogno, davvero, di auto per dichiarare una variabile che contiene una closure, poiché possiamo utilizzare un oggetto std:: function. Questo è vero, possiamo. Ma forse questo non è ciò che stavate pensando. Forse ciò che state pensando è “… e che cos’è questo oggetto std::function?”. Occorre chiarirlo.

std::function è un template nella Libreria Standard C++11 che generalizza l’idea di un puntatore a funzione. Mentre i puntatori a funzione possono puntare solo a funzioni, gli oggetti std::function possono fare riferimento a ogni oggetto richiamabile, ovvero qualsiasi cosa che possa essere richiamata con una funzione. Così come occorre specificare il tipo della funzione cui puntare quando si crea un puntatore a funzione (per esempio la signature delle funzioni cui si vuole puntare), occorre specificare il tipo di funzione cui fare riferimento quando si crea un oggetto std::function. Lo si può fare attraverso il parametro template di std::function. Per esempio, per dichiarare un oggetto std::function chiamato func che potrebbe fare riferimento a qualsiasi oggetto richiamabile e si comporta come se avesse la seguente signature:

bool(const std::unique_ptr<Widget>&, // signature C++11 per
const std::unique_ptr<Widget>&) // la funzione di confronto
  // std::unique_ptr<Widget>

si scriverebbe:

std::function<bool(const std::unique_ptr<Widget>&,

                const std::unique_ptr<Widget>&)> func;

Poiché le espressioni lambda forniscono oggetti richiamabili, le closure possono essere memorizzate in oggetti std::function. Ciò significa che si potrebbe dichiarare la versione C++11 di derefUPLess senza utilizzare auto nel seguente modo:

std::function<bool(const std::unique_ptr<Widget>&,

                   const std::unique_ptr<Widget>&)>

derefUPLess = [](const std::unique_ptr<Widget>& p1,

                  const std::unique_ptr<Widget>& p2)

                  { return *p1 < *p2; };

È importante riconoscere che anche trascurando la complessità sintattica e la necessità di ripetere i tipi di parametri, l’uso di std::function non equivale a usare auto. Una variabile dichiarata auto contenente una closure ha lo stesso tipo della closure e, in quanto tale, usa esattamente la stessa quantità di memoria richiesta dalla closure. Il tipo di una variabile dichiarata std::function che contiene una closure è un’istanziazione del template std::function e questo ha dimensioni fisse per una determinata signature. Queste dimensioni possono non essere adeguate per la closure che devono memorizzare e, in questo caso, il costruttore di std::function allocherà memoria sullo heap per memorizzare la closure. Il risultato è che l’oggetto std::function, in genere, userà più memoria rispetto all’oggetto dichiarato auto. E, grazie ai dettagli implementativi che limitano l’inlining e forniscono chiamate indirette a funzione, la chiamata di una closure tramite un oggetto std::function è quasi certamente più lenta rispetto a una chiamata attraverso un oggetto dichiarato auto. In altre parole, l’approccio con std::function è generalmente più “ingombrante” e lento rispetto all’approccio auto e può portare a problemi di esaurimento della memoria. In più, come si può vedere negli esempi precedenti, scrivere semplicemente “auto” è molto più semplice rispetto a scrivere il tipo dell’istanziazione std::function. Nella competizione fra auto e std::function per tenere una closure, c’è un solo vincitore: auto. Un argomento simile può essere fatto per auto rispetto a std::function per contenere il risultato di chiamate a std:: bind, ma nell’Elemento 34, farò del mio meglio per convincervi a utilizzare le lambda invece di std::bind.

I vantaggi di auto vanno ben oltre la possibilità di evitare le variabili non inizializzate, le dichiarazioni di variabili verbose e la possibilità di trattenere direttamente le closure. Uno è la possibilità di evitare i problemi legati alle “scorciatoie di tipo”. Ecco qualcosa che probabilmente avete già visto, e magari anche scritto:

std::vector<int> v;

unsigned sz = v.size();

Il tipo restituito, ufficialmente, da v.size() è std::vector<int>::size_type, ma pochi sviluppatori se ne rendono conto. std::vector<int>::size_type viene specificato come un tipo intero e senza segno, pertanto molti programmatori immaginano che basti sapere che std::vector<int>::size_type è un tipo intero unsigned e scrivono del codice come il precedente. Questo può avere alcune interessanti conseguenze. Nell’ambiente Windows a 32 bit, per esempio, unsigned e std::vector<int>::size_type hanno le stesse dimensioni, mentre in Windows a 64 bit, unsigned è di 32 bit, mentre std::vector<int>::size_type è di 64 bit. Ciò significa che il codice che funziona in Windows a 32 bit potrebbe comportarsi in modo errato in Windows a 64 bit e, nel passaggio di un’applicazione dai 32 ai 64 bit, chi vuole buttar via del tempo a risolvere problemi di questo tipo?

L’uso di auto garantisce di non dover ricorrere a:

auto sz = v.size();       // il tipo di sz è std::vector<int>::size_type

Ancora incerti sull’utilità di impiegare auto? Allora considerate il codice seguente:

std::unordered_map<std::string, int> m;

for (const std::pair<std::string, int>& p : m) {

{

…          // fa qualcosa con p

}

Sembra perfettamente accettabile, ma contiene un problema. Riuscite a individuarlo?

Occorrerà ricordarsi che l’elemento chiave di std::unordered_map è const, pertanto il tipo di std::pair nella tabella hash (quale è std::unordered_map), non è std:: pair<std::string, int>, è std::pair<const std::string, int>. Ma questo non è il tipo dichiarato per la variabile p nel ciclo. Come risultato, i compilatori cercheranno di trovare un modo per convertire gli oggetti std::pair<const std::string, int> (ovvero il contenuto della tabella hash) in oggetti std::pair<std::string, int> (il tipo dichiarato per p). Vi riusciranno creando un oggetto temporaneo del tipo cui p vuole legarsi copiando ciascun oggetto in m, poi collegando il riferimento p a questo oggetto temporaneo. Alla fine di ciascuna iterazione del ciclo, l’oggetto temporaneo verrà distrutto. Se provate a utilizzare questo ciclo, probabilmente resterete sorpresi dal suo comportamento, poiché, quasi certamente, intendevate semplicemente legare il riferimento p a ciascun elemento di m.

Questa differenza indesiderata di tipo può essere eliminata con auto:

for (const auto& p : m)

{

…            // come prima

}

Non solo questo è più efficiente, ma è anche più facile da scrivere. Inoltre, questo codice presenta il grande vantaggio che se si prende l’indirizzo di p, si è sicuri di ottenere un puntatore a un elemento all’interno di m. Nel codice che non usa auto, si otterrebbe un puntatore a un oggetto temporaneo, un oggetto che verrà poi distrutto, alla fine dell’iterazione del ciclo.

Gli ultimi due esempi, scrivere unsigned al posto di std::vector<int>::size_type e scrivere std::pair<std::string, int> al posto di std::pair<const std::string, int>) dimostrano come il fatto di specificare esplicitamente il tipo possa portare a conversioni implicite indesiderate e imprevedibili. Se si utilizza auto come tipo della variabile in questione, non è necessario preoccuparsi delle differenze fra il tipo della variabile dichiarata e il tipo dell’espressione utilizzata per inizializzarla.

Vi sono pertanto vari motivi che spingono a preferire auto rispetto a una dichiarazione esplicita del tipo. Tuttavia, neppure auto è perfetto. Il tipo di ciascuna variabile auto viene dedotto dalla sua espressione di inizializzazione e alcune espressioni di inizializzazione sono di un tipo imprevedibile e indesiderabile. Le condizioni in cui sorgono tali casi e che cosa si può fare al riguardo sono argomenti trattati negli Elementi 2 e 6, quindi non è il caso di ripetersi qui. Rivolgeremo invece l’attenzione a un altro problema che può sorgere utilizzando auto al posto di una dichiarazione tradizionale del tipo: la leggibilità del codice sorgente risultante.

Innanzitutto, fate un bel respiro e tranquillizzatevi. Usare auto è una scelta, non un obbligo. Se, secondo la vostra opinione professionale, il vostro codice sarebbe più chiaro o più facile da rielaborare o, per qualche altro motivo, migliore utilizzando dichiarazioni esplicite del tipo, sentitevi liberi di continuare a farlo. Ma tenete in considerazione che il C++ non introduce nulla di nuovo in ciò che, nel mondo dei linguaggi di programmazione, viene generalmente chiamato con il termine di inferenza del tipo. Altri linguaggi procedurali con tipo statico (per esempio C#, D, Scala, Visual Basic) hanno una funzionalità più o meno equivalente, per non parlare di un’intera varietà di linguaggi funzionali con tipo statico (per esempio ML, Haskell, OCaml, F# e così via). In parte, questo è dovuto al successo dei linguaggi a tipo dinamico, come Perl, Python e Ruby, dove le variabili ricevono raramente un tipo esplicito. La comunità degli sviluppatori software ha una grande esperienza nel campo dell’inferenza del tipo e ha dimostrato che non vi è nulla di contraddittorio fra questa tecnologia e la creazione e manutenzione di grandi basi di codice di livello professionale.

Alcuni sviluppatori sono disturbati dal fatto che l’utilizzo di auto impedisce di determinare il tipo di un oggetto semplicemente osservando il codice sorgente. Tuttavia, la capacità dell’IDE di mostrare il tipo di un oggetto, spesso allevia questo problema (anche tenendo in considerazione i problemi di visualizzazione del tipo negli IDE, menzionati nell’Elemento 4) e, in molti casi, un’indicazione astratta del tipo dell’oggetto può essere utile quanto un’indicazione esatta. Spesso basta, per esempio, sapere che un oggetto è un container o un contatore o un puntatore smart, senza sapere esattamente quale tipo di container, contatore o puntatore smart sia. Adottando nomi di variabile ben scelti, questa indicazione astratta del tipo può essere altrettanto “parlante”.

Il fatto è che, scrivendo esplicitamente il tipo, spesso si fa qualcosa di più che introdurre possibilità che sorgano errori subdoli, in termini sia di correttezza sia di efficienza (o di entrambe le cose). Inoltre, i tipi di auto cambiano automaticamente quando cambia il tipo dell’espressione utilizzata per la loro inizializzazione e ciò significa che alcune rielaborazioni vengono facilitate dall’uso di auto. Per esempio, se una funzione è dichiarata in modo che restituisca un int, ma successivamente si decide che sarebbe meglio impiegare un long, il codice chiamante si aggiornerà da solo alla successiva compilazione se i risultati della chiamata della funzione vengono memorizzati in variabili auto. Se invece i risultati vengono memorizzati in variabili dichiarate esplicitamente come int, sarete costretti a trovare tutte le chiamate per correggerle.

Argomenti da ricordare

•   Le variabili auto devono essere inizializzate, sono generalmente immuni agli errori di tipo che portano a problemi di portabilità o efficienza, possono facilitare il processo di rielaborazione del codice e, in genere, richiedono meno lavoro alla tastiera rispetto alle variabili il cui tipo è specificato esplicitamente.

•   Le variabili con tipo auto sono però soggette alle trappole descritte negli Elementi 2 e 6.

Elemento 6 – Uso di un inizializzatore di tipo esplicito quando auto deduce tipi indesiderati

Come spiega l’Elemento 5, impiegando auto per dichiarare le variabili si ottengono vari vantaggi tecnici rispetto all’indicazione esplicita del tipo, ma talvolta la deduzione del tipo provocata da auto è imprecisa. Per esempio, supponete di avere una funzione che prende un Widget e restituisce un std::vector<bool>, dove ciascun bool indica se Widget offre una determinata funzionalità:

std::vector<bool> features(const Widget& w);

Inoltre, supponete che il bit 5 indichi se il Widget ha una priorità elevata. Possiamo pertanto scrivere il seguente codice:

Widget w;  
 
bool highPriority = features(w)[5]; // w è ad alta priorità?
 
processWidget(w, highPriority); // elabora w in base
  // alla sua priorità

Non c’è niente di sbagliato in questo codice. Funzionerà perfettamente. Ma se eseguiamo una modifica apparentemente innocua, sostituendo auto al tipo esplicito di highPriority,

auto highPriority = features(w)[5];      // w è ad alta priorità?

la situazione cambia. Il codice continuerà a risultare compilabile, ma il suo comportamento non sarà più prevedibile:

processWidget(w, highPriority); // comportamento indefinito!

Come è indicato dal commento, la chiamata a processWidget ora ha un comportamento indefinito. Perché? La risposta sarà un po’ sorprendente. Nel codice che utilizza auto, il tipo di highPriority non è più bool. Anche se std::vector<bool> contiene concettualmente bool, operator[] per std::vector<bool> non restituisce un riferimento a un elemento del container (che è ciò che std::vector::operator[] restituisce per ogni tipo ad eccezione di bool). Al contrario, restituisce un oggetto di tipo std::vector<bool>::reference (una classe nidificata all’interno di std::vector<bool>).

std::vector<bool>::reference esiste poiché std::vector<bool> è specificato per rappresentare i suoi bool in una forma “a pacchetto”, un bit per bool. Ciò crea un problema per l’operator[] di std::vector<bool>, in quanto operator[], per std:: vector<T>, si suppone che restituisca un T&, mentre il C++ proibisce i riferimenti ai bit. Non potendo restituire un bool&, operator[] per std::vector<bool> restituisce un oggetto che si comporta come un bool&. Perché questo meccanismo possa funzionare, gli oggetti std::vector<bool>::reference devono essere utilizzabili sostanzialmente in tutti i contesti in cui possono essere utilizzati bool&. Fra le funzionalità di std::vector<bool>::reference che rendono possibile tutto questo, vi è una conversione implicita in bool. (Non in bool&, ma in bool. Spiegare tutte le tecniche impiegate da std::vector<bool>::reference per emulare il comportamento di un bool& richiederebbe troppo spazio, quindi diciamo semplicemente che questa conversione implicita è solo un’importante tessera di un mosaico ben più grande.)

Tenendo in considerazione questa informazione, osserviamo ancora una volta questa parte del codice originale:

bool highPriority = features(w)[5]; // dichiara esplicitamente
  // il tipo di highPriority

Qui, features restituisce un oggetto std::vector<bool>, sul quale viene richiamato operator[]. operator[] restituisce un oggetto std::vector<bool>::reference, che viene poi convertito implicitamente in bool, che è necessario per inizializzare highPriority. highPriority, pertanto, finisce per contenere il valore del bit 5 di std::vector<bool> restituito da features, esattamente come previsto.

Ecco invece che cosa accade nella dichiarazione con auto di highPriority:

auto highPriority = features(w)[5]; // deduce il tipo
  // di highPriority

Ancora una volta, features restituisce un oggetto std::vector<bool> e, ancora una volta, operator[] viene richiamato su di esso. operator[] continua a restituire un oggetto std::vector<bool>::reference, ma ora vi è una differenza, poiché auto lo deduce come tipo di highPriority. highPriority non contiene il valore del bit 5 del std::vector<bool> restituito da features.

Il valore che contiene dipende dal modo in cui è implementato std::vector<bool>::reference. Un’implementazione prevede che tali oggetti contengano un puntatore alla word che contiene il bit interessato, più l’offset, all’interno di tale word, per raggiungere questo bit. Considerate ciò che significa per l’inizializzazione di high-Priority, supponendo che sia attiva tale implementazione di std::vector<bool>::reference.

La chiamata a features restituisce un oggetto std::vector<bool> temporaneo. Questo oggetto non ha alcun nome, ma per gli scopi di questa discussione, lo chiameremo temp. operator[] viene richiamato su temp e il std::vector<bool>::reference che restituisce contiene un puntatore a una word nella struttura dei dati che contiene i bit gestiti da temp, più l’offset all’interno di tale word corrispondente al bit 5. highPriority è una copia di questo oggetto std::vector<bool>::reference, così anche highPriority contiene un puntatore a una word in temp, più l’offset corrispondente al bit 5. Alla fine dell’istruzione, temp viene distrutto, poiché si tratta di un oggetto temporaneo. Pertanto, highPriority contiene un puntatore pendente e questo è il motivo del comportamento indefinito nella chiamata a processWidget:

processWidget(w, highPriority); // comportamento indefinito!
  // highPriority contiene
  // un puntatore pendente!

std::vector<bool>::reference è un esempio di classe proxy: una classe che esiste con lo scopo di emulare ed estendere il comportamento di qualche altro tipo. Le classi proxy vengono impiegate per vari scopi. std::vector<bool>::reference esiste per offrire l’illusione che operator[] per std::vector<bool> restituisca un riferimento a un bit, per esempio, e i tipi per puntatori smart della Libreria Standard (vedere il Capitolo 4) sono classi proxy che applicano la gestione delle risorse ai comuni puntatori. L’utilità delle classi proxy è indiscutibile. In pratica, lo stesso concetto di “proxy” è molto affermato nel “Pantheon” dello sviluppo software.

Alcune classi proxy sono progettate per essere visibili ai client. Questo è il caso di std:: shared_ptr e std::unique_ptr, per esempio. Altre classi proxy sono progettate per comportarsi in modo più o meno invisibile. std::vector<bool>::reference è un esempio di questi proxy “invisibili”, così come il suo compagno std::bitset::reference.

Inoltre, sul terreno vi sono alcune classi delle librerie C++ che impiegano una tecnica nota come expression template. Tali librerie sono state originariamente sviluppate per migliorare l’efficienza del codice numerico. Per esempio, data una classe Matrix e degli oggetti m1, m2, m3 e m4 di tipo Matrix, l’espressione

Matrix sum = m1 + m2 + m3 + m4;

può essere calcolata in modo molto più efficiente se operator+ per gli oggetti Matrix restituisce un proxy per il risultato invece del risultato stesso. In pratica, operator+ per due oggetti Matrix restituirà un oggetto di una classe Sum<Matrix, Matrix> invece che un oggetto di tipo Matrix. Come è stato il caso di std::vector<bool>::reference e bool, vi sarebbe una conversione implicita dalla classe proxy a Matrix, che permetterebbe l’inizializzazione di sum dall’oggetto proxy prodotto dall’espressione che si trova sul lato destro del segno “=”. Il tipo di tale oggetto codificherebbe tradizionalmente l’intera espressione di inizializzazione, ovvero sarebbe qualcosa come Sum<Sum<Sum<Matrix, Matrix> che è certamente un tipo da cui i client dovrebbero essere protetti.

Come regola generale, le classi proxy “invisibili” non vanno troppo d’accordo con auto. Gli oggetti di queste classi, in genere, non sono progettati per vivere più a lungo di un’unica istruzione e dunque la creazione di variabili di questi tipi tende a violare le assunzioni progettuali fondamentali della libreria. Questo è il caso di std::vector<-bool>::reference e abbiamo visto che violare tale assunto può portare a comportamenti indefiniti.

Pertanto si deve evitare di scrivere codice nella seguente forma:

auto someVar = espressione di un tipo classe proxy "invisibile";

Ma come si può riconoscere quando sono attivi degli oggetti proxy? Il software che li impiega non mostra affatto la loro esistenza. Si suppone che siano invisibili, almeno concettualmente! E una volta che li abbiamo trovati, dobbiamo davvero abbandonare auto e i tanti vantaggi evidenziati dall’Elemento 5 solo per questo motivo?

Affrontiamo innanzitutto il problema di trovarli. Anche se le classi proxy “invisibili” sono progettate per non comparire nel “radar” del programmatore nell’utilizzo quotidiano, le librerie che le utilizzano spesso ne documentano la presenza. Più familiarizzerete con le decisioni progettuali che stanno alla base delle librerie che impiegate, più saprete quale uso viene fatto dei proxy in queste librerie.

Quando anche la documentazione non aiuta, basta cercare nei file header. Raramente il codice sorgente riesce a nascondere completamente l’impiego di oggetti proxy. Vengono normalmente restituiti da funzioni che si prevede che vengano chiamate dai client, dunque la signature della funzione normalmente riflette la loro esistenza. Ecco, per esempio, l’aspetto di std::vector<bool>::operator[]:

namespace std {        // standard C++

 

template <class Allocator>

class vector<bool, Allocator> {

public:

class reference { };

 

reference operator[](size_type n);

};

}

Supponendo di sapere che operator[] per std::vector<T> normalmente restituisce un T&, il tipo non convenzionale restituito per operator[] in questo caso è un indizio dell’impiego di una classe proxy. Facendo molta attenzione alle interfacce utilizzate, spesso è possibile individuare l’esistenza delle classi proxy.

In pratica, molti sviluppatori scoprono l’uso delle classi proxy solo quando tentano di venire a capo di strani problemi di compilazione o di eseguire il debugging di risultati errati dei test. Indipendentemente dal modo in cui le trovate, una volta che avete determinato che auto deduce il tipo di una classe proxy invece del tipo cui fa riferimento la classe proxy, la soluzione non prevede necessariamente l’abbandono di auto. Il problema non è, infatti, dovuto all’auto. Il problema è che auto non deduce il tipo che noi vogliamo che deduca. La soluzione consiste nel costringerlo a impiegare un’altra deduzione del tipo. Il modo per farlo può essere chiamato inizializzatore con tipo esplicito.

Un inizializzatore con tipo esplicito prevede la dichiarazione di una variabile con auto, convertendo però l’espressione di inizializzazione nel tipo che auto dovrà poi dedurre.

Ecco come si può utilizzare questa sintassi per costringere highPriority a essere, per esempio, un bool:

auto highPriority = static_cast<bool>(features(w)[5]);

Qui, features(w)[5] continua a restituire a un oggetto std::vector<bool>::reference, come ha sempre fatto, ma la conversione (static_cast) cambia il tipo dell’espressione in un bool, che auto, poi, deduce essere il tipo di highPriority. Runtime, l’oggetto std::vector<bool>::reference restituito da std::vector<-bool>::operator[] esegue la conversione in bool che supporta e, nell’ambito di tale conversione, viene referenziato il, comunque valido, puntatore a std::vector<bool> restituito da features. Ciò evita il comportamento indefinito che abbiamo visto in precedenza. Ai bit puntati dal puntatore viene poi applicato l’indice 5 e il valore bool che ne emerge viene infine utilizzato per inizializzare highPriority.

Quanto all’esempio di Matrix, l’inizializzatore con tipo esplicito avrebbe il seguente aspetto:

auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

Gli usi di questa forma non si limitano agli inizializzatori che forniscono tipi costituiti da classi proxy. Possono essere utili anche per chiarire il fatto che si sta creando deliberatamente una variabile di un tipo che è differente da quello generato dall’espressione di inizializzazione. Per esempio, supponete di avere una funzione per calcolare un determinato valore di tolleranza:

double calcEpsilon(); // restituisce il valore della tolleranza

calcEpsilon restituisce chiaramente un double, ma supponete di sapere che per la vostra applicazione sia adeguata la precisione di un float e che per voi conti la differenza dimensionale esistente fra un float e un double. Potreste dichiarare una variabile float per memorizzare il risultato di calcEpsilon,

float ep = calcEpsilon(); // conversione implicita
  // double → float

Ma questo non comunica esattamente “Sto riducendo deliberatamente la precisione del valore restituito dalla funzione”. Una dichiarazione che utilizza l’inizializzatore con tipo esplicito, invece, chiarisce questo fatto:

auto ep = static_cast<float>(calcEpsilon());

Si applica un ragionamento simile quando si ha un’espressione in virgola mobile che si memorizza deliberatamente sotto forma di un valore intero. Supponete di dover calcolare gli indici di un elemento in un container con degli iteratori ad accesso casuale (per esempio un std::vector, std::deque o std::array) e di ricevere un double compreso fra 0.0 e 1.0 che indica la distanza rispetto all’inizio del container in cui è situato l’elemento desiderato (0.5 indicherebbe, quindi, il centro del container). Inoltre, supponete di pensare che l’indice risultante sia adatto alla memorizzazione in un int. Se il container è c e il double è d, potreste calcolare l’indice nel seguente modo,

int index = d * c.size();

ma ciò nasconderebbe il fatto che state intenzionalmente convertendo il double a destra in modo che diventi un int. L’inizializzatore con tipo esplicito chiarisce questo fatto:

auto index = static_cast<int>(d * c.size());

Argomenti da ricordare

•   I tipi proxy “invisibili” possono far sì che auto deduca il tipo “errato” per un’espressione di inizializzazione.

•   L’inizializzatore con tipo esplicito costringe auto a dedurre il tipo desiderato.