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:
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.