Alias di Tipi, auto e decltype in C++
- Gli alias di tipo semplificano la scrittura di tipi complessi.
- Si può utilizzare sia il metodo tradizionale
typedef
, sia la nuova parola chiaveusing
. - Lo specificatore
auto
consente al compilatore di dedurre il tipo di una variabile. - Lo specificatore
decltype
permette di ottenere il tipo di un'espressione.
Man mano che i nostri programmi diventano più complicati, vedremo che anche i tipi che usiamo diventano più complicati.
Le complicazioni nell'uso dei tipi sorgono in due modi diversi. Alcuni tipi sono difficili da "scrivere". Cioè, hanno forme che sono noiose e soggette a errori da scrivere. Inoltre, la forma di un tipo complicato può oscurare il suo scopo o significato.
L'altra fonte di complicazione è che a volte è difficile determinare il tipo esatto di cui abbiamo bisogno. Farlo può richiedere di guardare indietro nel contesto del programma.
Per questo motivo, il linguaggio C++ mette a disposizione tre meccanismi per far in modo che sia il compilatore stesso, nel momento in cui esamina il nostro programma, a determinare quale sia il tipo corretto di una variabile.
Tali meccanismi sono: l'alias di tipi, lo specificatore auto
e lo specificatore decltype
. Vediamo come funzionano.
Alias di Tipi
Un alias di tipo è un nome che è un sinonimo per un altro tipo.
Gli alias di tipo ci permettono di semplificare definizioni di tipo complicate, rendendo quei tipi più facili da usare. Gli alias di tipo ci permettono anche di enfatizzare lo scopo per cui un tipo viene usato.
Possiamo definire un alias di tipo in uno di due modi.
Il metodo tradizionale, mutuato dal linguaggio C, usa typedef
:
// stipendio è un sinonimo per double
typedef double stipendio;
// salario è un sinonimo per double, p per double*
typedef stipendio salario, *p;
La parola chiave typedef
può apparire come parte del tipo base di una dichiarazione. Le dichiarazioni che includono typedef
definiscono alias di tipi piuttosto che variabili. Come in qualsiasi altra dichiarazione, i dichiaratori possono includere modificatori di tipo che definiscono tipi composti costruiti dal tipo base della definizione.
Lo standard C++11 ha introdotto un secondo modo per definire un alias di tipo, tramite una dichiarazione alias:
using AV = ArticoloVendita; // AV è un sinonimo per ArticoloVendita
Una dichiarazione alias inizia con la parola chiave using
seguita dal nome dell'alias e un =
. La dichiarazione alias definisce il nome sul lato sinistro del =
come alias per il tipo che appare sul lato destro.
Un alias di tipo è un nome di tipo e può apparire ovunque possa apparire un nome di tipo:
stipendio orario, settimanale; // stesso che double orario, settimanale;
AV articolo; // stesso che ArticoloVendita articolo
Preferire using
a typedef
Il metodo typedef
è più vecchio e più familiare a molti programmatori, soprattutto per il fatto che deriva dal linguaggio C.
Tuttavia, using
è più potente e flessibile di typedef
, specialmente quando si lavora con template. Per questo motivo, è generalmente consigliabile preferire using
a typedef
per definire alias di tipi in C++11 e versioni successive.
Quindi nel nuovo codice C++ è preferibile usare using
per definire alias di tipi.
Puntatori, const
e Alias di Tipi
Le dichiarazioni che usano alias di tipi che rappresentano tipi composti e const
possono produrre risultati sorprendenti. Per esempio, le seguenti dichiarazioni usano il tipo pstring
, che è un alias per il tipo char *
:
typedef char *pstring;
// cstr è un puntatore costante a char
const pstring cstr = 0;
// ps è un puntatore a un puntatore costante a char
const pstring *ps;
Il tipo base in queste dichiarazioni è const pstring
. Come al solito, un const
che appare nel tipo base modifica il tipo dato. Il tipo di pstring
è "puntatore a char". Quindi, const pstring
è un puntatore costante a char
e non un puntatore a const char
.
Si può essere tentati, anche se è errato, interpretare una dichiarazione che usa un alias di tipo sostituendo concettualmente l'alias con il suo tipo corrispondente:
// interpretazione errata di const pstring cstr
const char *cstr = 0;
Tuttavia, questa interpretazione è sbagliata. Quando usiamo pstring
in una dichiarazione, il tipo base della dichiarazione è un tipo puntatore. Quando riscriviamo la dichiarazione usando char *
, il tipo base è char
e l'asterisco *
fa parte del dichiaratore. In questo caso, const char
è il tipo base. Questa riscrittura dichiara cstr
come un puntatore a const
char piuttosto che come un puntatore const
a char
.
Lo specificatore di tipo auto
Non è raro voler memorizzare il valore di un'espressione in una variabile. Per dichiarare la variabile, dobbiamo conoscere il tipo di quell'espressione.
Quando scriviamo un programma, può essere sorprendentemente difficile, e a volte persino impossibile, determinare il tipo di un'espressione. Sotto lo standard C++11, possiamo lasciare che il compilatore deduca il tipo per noi usando lo specificatore di tipo auto
. A differenza degli specificatori di tipo, come double
, che nominano un tipo specifico, auto
dice al compilatore di dedurre il tipo dall'inizializzatore. Per implicazione, una variabile che usa auto
come suo specificatore di tipo deve avere un inizializzatore:
// il tipo di articolo è dedotto dal tipo del risultato
// della somma di val1 e val2
auto articolo = val1 + val2;
Qui il compilatore dedurrà il tipo di articolo dal tipo restituito applicando l'operatore +
a val1
e val2
. Se val1
e val2
sono oggetti di un certo tipo x
, articolo avrà tipo x
. Se quelle variabili sono tipo double
, allora articolo ha tipo double
, e così via.
Come con qualsiasi altro specificatore di tipo, possiamo definire più variabili usando auto
. Poiché una dichiarazione può coinvolgere solo un singolo tipo base, gli inizializzatori per tutte le variabili nella dichiarazione devono avere tipi che siano coerenti tra loro:
// OK: i è int e p è un puntatore a int
auto i = 0, *p = &i;
// ERRORE: tipi incoerenti per sz e pi
auto sz = 0, pi = 3.14;
Tipi Composti, const
e auto
Il tipo che il compilatore deduce per auto
non è sempre esattamente lo stesso del tipo dell'inizializzatore. Invece, il compilatore aggiusta il tipo per conformarsi alle normali regole di inizializzazione.
Primo, come abbiamo visto, quando usiamo un riferimento, stiamo realmente usando l'oggetto a cui il riferimento si riferisce. In particolare, quando usiamo un riferimento come inizializzatore, l'inizializzatore è l'oggetto corrispondente. Il compilatore usa il tipo di quell'oggetto per la deduzione del tipo di auto
:
int i = 0, &r = i;
// a è un int (r è un alias per i, che ha tipo int)
auto a = r;
Secondo, auto
ordinariamente ignora il const
di alto livello. Come al solito nelle inizializzazioni, le const
di basso livello, come quando un inizializzatore è un puntatore a const
, sono mantenute:
const int ci = i, &cr = ci;
// b è un int (const di alto livello in ci è eliminata)
auto b = ci;
// c è un int (cr è un alias per ci la cui const è di alto livello)
auto c = cr;
// d è un int* (& di un oggetto int è int*)
auto d = &i;
// e è const int* (& di un oggetto const è const di basso livello)
auto e = &ci;
Se vogliamo che il tipo dedotto abbia una const
di alto livello, dobbiamo dirlo esplicitamente:
// tipo dedotto di ci è int; f ha tipo const int
const auto f = ci;
Possiamo anche specificare che vogliamo un riferimento al tipo dedotto da auto
. Si applicano le normali regole di inizializzazione:
// g è un const int& che è legato a ci
auto &g = ci;
// ERRORE: non possiamo legare un riferimento normale a un letterale
auto &h = 42;
// OK: possiamo legare un riferimento const a un letterale
const auto &j = 42;
Quando chiediamo un riferimento a un tipo dedotto da auto
, le const
di alto livello nell'inizializzatore non sono ignorate. Come al solito, le const
non sono di alto livello quando leghiamo un riferimento a un inizializzatore.
Quando definiamo diverse variabili nella stessa istruzione, è importante ricordare che un riferimento o puntatore fa parte di un particolare dichiaratore e non fa parte del tipo base per la dichiarazione. Come al solito, gli inizializzatori devono fornire tipi dedotti da auto coerenti:
// k è int; l è int&
auto k = ci, &l = i;
// m è un const int&; p è un puntatore a const int
auto &m = ci, *p = &ci;
// ERRORE: tipo dedotto da i è int; tipo dedotto da &ci è const int
auto &n = i, *p2 = &ci;
Esercizi
-
Usando le definizioni di variabili seguenti:
int i = 0, &r = i; auto a = r; const int ci = i, &cr = ci; auto b = ci; auto c = cr; auto d = &i; auto e = &ci; const auto f = ci; auto &g = ci;
determina cosa succede in ciascuna di queste assegnazioni:
a = 42; b = 42; c = 42; d = 42; e = 42; g = 42;
Soluzione:
a = 42;
Valida.a
è di tipoint
, quindi può essere assegnato il valore42
.b = 42;
Valida.b
è di tipoint
, quindi può essere assegnato il valore42
.c = 42;
Valida.c
è di tipoint
, quindi può essere assegnato il valore42
.d = 42;
Non valida.d
è di tipoint*
(puntatore a int), quindi non può essere assegnato un valore intero diretto come42
. Dovrebbe essere assegnato un indirizzo.e = 42;
Non valida.e
è di tipoconst int*
(puntatore a const int), quindi non può essere assegnato un valore intero diretto come42
. Dovrebbe essere assegnato un indirizzo.g = 42;
Non valida.g
è un riferimento aconst int
, quindi non può essere modificato dopo l'inizializzazione.
-
Determina i tipi dedotti in ciascuna delle seguenti definizioni.
const int i = 42; auto j = i; const auto &k = i; auto *p = &i; const auto j2 = i, &k2 = i;
Soluzione:
j
è di tipoint
. Laconst
di alto livello ini
viene ignorata.k
è di tipoconst int&
. È un riferimento ai
, che èconst int
.p
è di tipoconst int*
. È un puntatore ai
, che èconst int
.j2
è di tipoconst int
. Laconst
di alto livello ini
viene mantenuta.k2
è di tipoconst int&
. È un riferimento ai
, che èconst int
.
Lo specificatore di tipo decltype
A volte vogliamo definire una variabile con un tipo che il compilatore deduce da un'espressione ma non vogliamo usare quell'espressione per inizializzare la variabile.
Per tali casi, lo standard C++11 ha introdotto un secondo specificatore di tipo, decltype
, che restituisce il tipo del suo operando. Il compilatore analizza l'espressione per determinare il suo tipo ma non valuta l'espressione:
// somma ha qualunque tipo f restituisca
decltype(f()) somma = x;
Qui, il compilatore non chiama la funzione f
, ma usa il tipo che tale chiamata restituirebbe come tipo per somma. Cioè, il compilatore dà a somma lo stesso tipo che sarebbe restituito se chiamassimo f
.
Il modo in cui decltype
gestisce const
di alto livello e riferimenti differisce sottilmente dal modo in cui lo fa auto
. Quando l'espressione a cui applichiamo decltype
è una variabile, decltype
restituisce il tipo di quella variabile, inclusi const
di alto livello e riferimenti:
const int ci = 0, &cj = ci;
// x ha tipo const int
decltype(ci) x = 0;
// y ha tipo const int& ed è legato a x
decltype(cj) y = x;
// errore: z è un riferimento e deve essere inizializzato
decltype(cj) z;
Poiché cj
è un riferimento, decltype(cj)
è un tipo riferimento. Come qualsiasi altro riferimento, z
deve essere inizializzato.
Vale la pena notare che decltype
è l'unico contesto in cui una variabile definita come riferimento non è trattata come sinonimo per l'oggetto a cui si riferisce.
decltype
e Riferimenti
Quando applichiamo decltype
a un'espressione che non è una variabile, otteniamo il tipo che quell'espressione produce. Come vedremo nelle prossime lezioni, alcune espressioni faranno sì che decltype
produca un tipo riferimento. In generale, decltype
restituisce un tipo riferimento per espressioni che producono oggetti che possono stare sul lato sinistro dell'assegnazione:
// decltype di un'espressione può essere un tipo riferimento
int i = 42, *p = &i, &r = i;
// OK: l'addizione produce un int; b è un int (non inizializzato)
decltype(r + 0) b;
// ERRORE: c è int& e deve essere inizializzato
decltype(*p) c;
Qui r
è un riferimento, quindi decltype(r)
è un tipo riferimento. Se vogliamo il tipo a cui r
si riferisce, possiamo usare r
in un'espressione, come r + 0
, che è un'espressione che produce un valore che ha un tipo non riferimento.
D'altra parte, l'operatore di dereferenziazione è un esempio di espressione per cui decltype
restituisce un riferimento. Come abbiamo visto, quando dereferenziamo un puntatore, otteniamo l'oggetto a cui il puntatore punta. Inoltre, possiamo assegnare a quell'oggetto. Quindi, il tipo dedotto da decltype(*p)
è int&
, non int
semplice.
Un'altra importante differenza tra decltype
e auto
è che la deduzione fatta da decltype
dipende dalla forma della sua espressione data. Ciò che può essere confuso è che racchiudere il nome di una variabile tra parentesi influisce sul tipo restituito da decltype
. Quando applichiamo decltype
a una variabile senza parentesi, otteniamo il tipo di quella variabile. Se avvolgiamo il nome della variabile in uno o più set di parentesi, il compilatore valuterà l'operando come un'espressione. Una variabile è un'espressione che può essere il lato sinistro di un'assegnazione. Di conseguenza, decltype
su tale espressione produce un riferimento:
// decltype di una variabile tra parentesi è sempre un riferimento
// ERRORE: d è int& e deve essere inizializzato
decltype((i)) d;
// OK: e è un int (non inizializzato)
decltype(i) e;
decltype
e Parentesi
Ricorda che decltype((variabile))
(nota, doppie parentesi) è sempre un tipo riferimento, ma decltype(variabile)
è un tipo riferimento solo se variabile
è un riferimento.
Esercizi
-
Nel seguente codice, determina il tipo di ogni variabile e il valore che ogni variabile ha quando il codice finisce:
int a = 3, b = 4; decltype(a) c = a; decltype((b)) d = a; ++c; ++d;
Per quanto riguarda il tipo abbiamo che:
c
è di tipoint
perchédecltype(a)
;d
è di tipoint&
perchédecltype((b))
(con doppie parentesi) restituisce un riferimento al tipo dib
.
Quindi il codice diventa:
int a = 3, b = 4; int c = a; // c è 3 int& d = a; // d è un riferimento a a, quindi d è 3 ++c; // c diventa 4 ++d; // a diventa 4, quindi d è 4
Il valore finale delle variabili è:
a
è 4b
è 4, nessuna modificac
è 4d
è 4 (riferito aa
)
-
L'assegnazione è un esempio di espressione che produce un tipo riferimento. Il tipo è un riferimento al tipo dell'operando sinistro. Cioè, se
i
è unint
, allora il tipo dell'espressionei=x
èint&
. Usando quella conoscenza, determina il tipo e il valore di ogni variabile in questo codice:int a = 3, b = 4; decltype(a) c = a; decltype(a = b) d = a;
Per quanto riguarda il tipo abbiamo che:
c
è di tipoint
perchédecltype(a)
restituisce il tipo dia
, che èint
.d
è di tipoint&
perchédecltype(a = b)
restituisce un riferimento al tipo dia
, che èint
.
-
Descrivi le differenze nella deduzione del tipo tra decltype e auto. Fornisci un esempio di un'espressione dove auto e decltype dedurranno lo stesso tipo e un esempio dove dedurranno tipi diversi.
La differenza principale tra
decltype
eauto
nella deduzione del tipo è chedecltype
restituisce il tipo esatto dell'espressione data, inclusiconst
di alto livello e riferimenti, mentreauto
ignora leconst
di alto livello e può dedurre un tipo non riferimento anche se l'inizializzatore è una variabile di riferimento.Esempio dove
auto
edecltype
dedurranno lo stesso tipo:int x = 10; auto a = x; // a è di tipo int decltype(x) b = x; // b è di tipo int
In questo caso, sia
a
cheb
sono di tipoint
.Esempio dove
auto
edecltype
dedurranno tipi diversi:const int y = 20; auto c = y; // c è di tipo int (const di alto livello ignorata) decltype(y) d = y; // d è di tipo const int
In questo caso,
c
è di tipoint
, mentred
è di tipoconst int
.