In un linguaggio fortemente tipizzato come il C++, la conversione di tipo (type casting) è un meccanismo fondamentale per la corretta esecuzione dei programmi.
La sola rappresentazione in forma binaria di una variabile o di un letterale, infatti, non basta a definire il suo valore. L'informazione che riguarda il tipo consente di distinguere un numero intero da uno in virgola mobile o da un carattere. Una particolare sequenza di bit può essere interpretata come una stringa di testo o la rappresentazione binaria di una struttura dati definita dal programmatore.
Esistono due modalità di conversione: quelle che il compilatore effettua in autonomia (implicite), e quelle definite dall'utente (esplicite).
Poiché la conversione di tipo espone il programma a notevoli criticità, inclusa una terminazione prematura, il linguaggio predispone una serie di strumenti sintattici per definire minuziosamente la politica di conversione per i tipi primitivi e non.
Conversioni implicite
Il compilatore effettua una conversione implicita di tipo ogni qualvolta il tipo nominale di un dato non corrisponde a quello atteso in un determinato contesto, ad esempio la valutazione di un'espressione o una chiamata a funzione, ma è definita una sequenza di operazioni che consente di convertire da un tipo all'altro.
Ad esempio, il meccanismo di conversioni implicite consente di assegnare un letterale intero ad una variabile floating point e vice versa senza che ciò degeneri in una condizione di errore a tempo di compilazione.
Nel caso di conversioni tra tipi numerici, cioè quelli primitivi del linguaggio, si parla più precisamene di promozione o di conversione di tipo a seconda del rapporto che sussiste tra l'intervallo e la distribuzione di valori rappresentabili del tipo di origine e di quello di destinazione.
Ad esempio, nel frammento seguente il valore intero 123 viene promosso in un long int
ed assegnato ad a ed il valore in virgola mobile 3.4 viene convertito in intero ed assegnato a b:
long int a = 123; // promozione
int b = 3.4; // conversione
La differenza consiste nel fatto che una promozione non comporta perdita di informazione, mentre una conversione è potenzialmente esposta a questo rischio, come per la perdita di precisione implicita nel passaggio tra valore decimale e intero.
Conversioni esplicite
Una conversione di tipo viene detta esplicita quando è il programmatore a imporla mediante l'uso esplicito di un costrutto sintattico per la conversione. Una conversione esplicita può articolarsi in una sequenza di più conversione, alcune delle quali possono anche avvenire implicitamente.
Conversioni in stile C
Il linguaggio C++ eredita dal linguaggio C l'operatore di casting (basato sull'uso delle parentesi tonde), che consente di effettuare una conversione di tipo esplicita e arbitraria. Nel frammento seguente, ad esempio, un valore decimale è esplicitamente convertito in intero:
int b = (int) 3.4; // conversione esplicita in stile C
Tra gli strumenti di conversione, l'operatore di casting e sicuramente quello più potente, ma anche quello che espone a più criticità poiché esso è esente da qualsiasi controllo a tempo di compilazione. Ad esempio è possibile scrivere una sequenza di istruzioni come la seguente, che determina una corruzione di memoria:
const char* a = {"hello world!"};
double* ptr = (double*) (&a);
*ptr = 1234.5; // corruzione di memoria!
Il meccanismo di inferenza dei tipi di un compilatore C++ è perfettamente in grado di rilevare il problema, ma esso rimane silente quando si fa ricorso a questo operatore per motivi di retro-compatibilità. Pertanto l'uso di quest'ultimo, nel contesto della programmazione in C++, dovrebbe essere relegato alla sola integrazione di sorgenti in puro C.
Vedremo meglio nelle lezioni seguenti che il suo uso è infatti deprecato in favore di costrutti sintattici più evoluti.
Conversioni di tipi complessi
La conversione non è relegata ai soli tipi standard. Infatti esistono molteplici costrutti anche per la conversione di oggetti. In questo contesto esamineremo i costrutti impiegati per la conversione che sono parte integrante della definizione di una classe.
Costruttori e operatori di conversione
Prima dell'introduzione dello standard C++11, i costruttori dotati di un solo argomento non opzionale venivano definiti costruttori di conversione. A seguito delle nuove modalità di inizializzazione introdotte dal nuovo standard, ogni costruttore indipendentemente dal numero dei suoi argomenti, (incluso quello di default, quello di copia e di spostamento) è per estensione considerato un costruttore di conversione.
Inoltre è possibile definire anche appositi operatori di conversione come membri di classe che consentono di definire le modalità di conversione implicita di un oggetto, la cui sintassi è generalmente la seguente, in cui TypeName è il tipo di destinazione:
operator TypeName () const { ... }
Nel listato seguente, la classe A è dotata di molteplici costruttori e operatori di conversione implicita da e verso i tipi int
e bool
. Nella funzione main() è dato un esempio di inizializzazione a mediante l'uso del costruttore di conversione da bool
e dei due operatori definiti.
#include <iostream>
class A
{
public:
// costruttore di conversione da int
A(int i) { val = i; }
// costruttore di conversione da bool
A(bool flag) { val = flag ? 100 : -100; }
// operatore di conversione a bool
operator bool () const { return (val > 0); }
// operatore di conversione a int
operator int () const { return val; }
protected:
int val;
};
int main()
{
// costruttore di conversione
A var = true;
// operatori di conversione
if (var)
std::cout << var + 5 << "\n";
return 0;
}
Uso della parola chiave explicit
È possibile inibire la capacità dal compilatore di effettuare conversioni implicita mediante l'uso della parola chiave explicit
.
Quando essa è preposta alla dichiarazione di un costruttore o di un operatore di conversione (a partire dallo standard c++11) essa garantisce che il compilatore non usi tali costrutti in forma implicita, trasferendo al programmatore l'onere di indicare quando una conversione è richiesta. Ad esempio, nel listato seguente la parola chiave explicit
è usata per la dichiarazione dei metodi della classe A, e ciò determina la necessità di richiamarne in modo esplicito sia i costruttori che gli operatori di conversione al fine di prevenire errori di compilazione.
#include <iostream>
class A
{
public:
// costruttore di conversione da int
explicit A(int i) { val = i; }
// costruttore di conversione da bool
explicit A(bool flag) { val = flag ? 100 : -100; }
// operatore di conversione a bool
explicit operator bool () const { return (val > 0); }
// operatore di conversione a int
explicit operator int () const { return val; }
protected:
int val;
};
int main()
{
// costruttore di conversione
A var(true);
// var è "contestualmente convertibile" in bool
// in modo implicito, anche se usiamo explicit!
if (var)
std::cout << int(var) + 5 << "\n";
return 0;
}
Priorità tra operatori di conversione e casi particolari
L'uso di operatori di conversione verso tipi numerici interi in realtà è soggetto ad alcune criticità. Ad esempio, è buona norma applicare a tutti questi operatori il qualificatore const, poichè in linea di principio una conversione non dovrebbe apportare modifiche all'oggetto in questione, e in secondo luogo poichè in caso di ambiguità, ad esempio in un contesto in cui può essere ugualmente valida una conversione a booleano o a intero, il compilatore privilegia l'uso di operatori non const-qualified. In caso di differente qualifica degli operatori ciò potrebbe produrre risultati inattesi.
Inoltre, l'uso dello specificatore explicit
per l'operatore di conversione booleano presenta alcune peculiarità. Infatti, nel listato precedente, la conversione di var a valore booleano è implicita nonostante l'operatore in questione sia dichiarato con explicit
. Per la conversione a intero invece siamo costretti a indicare esplicitamente la conversione. Ciò accade perché lo standard c++11 ha introdotto la definizione di espressione contestualmente convertibile a booleano per evitare che in contesti in cui è inequivocabilmente richiesta la conversione a bool
si sia costretti a specificare la conversione esplicitamente. Questi contesti sono tipicamente le condizioni logiche delle strutture del controllo di flusso (if, for e while), gli operatori logici (&&, || e !) e l'operatore ternario.
In tutti gli altri contesti, ad esempio l'assegnamento, l'uso di explicit
impedisce che venga effettuata una conversione implicita che potrebbe essere indesiderata, senza la necessità di ricorrere a pattern verbosi come il safe bool idiom.
Conversioni di puntatori con controllo dei tipi
Abbiamo visto nel caso dell'operatore di casting in stile C che una delle principali criticità della conversione di tipo in linguaggio C++ è relativa al caso specifico della conversione di puntatori.
La conversione di un valore può infatti essere mediata da opportuni strumenti quali i costruttori e gli operatori di conversione, ma convertire un puntatore da un tipo ad un altro è un'operazione potenzialmente rischiosa se effettuata su variabili non compatibili.
Inoltre anche i qualificatori giocano un ruolo importante della definizione di un tipo, e pertanto un uso accorto degli strumenti di conversione deve tenerne conto.
Il linguaggio C++ predispone degli operatori specifici per la conversione di tipo che implementano varie modalità di controllo che, come vedremo nel seguito, si adattano a casi d'uso specifici e consentono di aumentare la leggibilità e la robustezza generale del codice.