Il qualificatore const
in C++ ha molti usi e declinazioni, a seconda che sia applicato ad una variabile, ad un metodo di una classe, agli argomenti o al valore di ritorno di una funzione.
Come il nome stesso suggerisce, const
rende l'entità cui è applicato immutabile (nel caso, ad esempio, di una variabile), o indica che l'entità stessa non può applicare cambiamenti (come nel caso dei metodi di una classe). L'uso più ovvio è quello della definizione di valori costanti che rimangono immutati per l'intera esecuzione del programma, ad esempio costanti numeriche:
const double phi = 1.6180339887;
Data l'immutabilità di una variabile const
, la sua inizializzazione deve essere contestuale alla dichiarazione. Ad esempio il frammento seguente genererà un errore di compilazione:
const double phi;
phi = 1.6180339887; // errore
Si osservi che il concetto di "variabile costante" non è un ossimoro: si ricordi che una variabile, nel contesto dei linguaggi di programmazione, è solo una locazione di memoria cui è associato un nome, a prescindere dal fatto che il suo contenuto muti durante l'esecuzione del programma.
Dichiarazione di puntatori
Nell'ambito della dichiarazione di puntatori, anche la posizione del qualificatore const
riveste la sua importanza, poiché assume valenze semantiche differenti. Nel listato seguente, ad esempio, compaiono tre modalità di dichiarazione di un puntatore a intero che fanno uso di questo qualificatore in modi differenti:
int main()
{
int a = 111;
int b = 222;
const int* ptr1 = &a;
//*ptr1 = 333; // errore: la locazione puntata da ptr è immutabile
ptr1 = &b; // ma il puntatore ptr è modificabile.
int* const ptr2 = &a;
// ptr2 = &b; // errore: il puntatore ptr2 è immutabile
*ptr2 = 444; // ma non la locazione cui punta.
const int* const ptr3 = &a;
//ptr3 = &b; // errore: il puntatore ptr3 non è modificabile
//*ptr3 = 555; // errore: non lo è nemmeno la locazione di memoria da esso puntata
return 0;
}
Il caso più ricorrente è sicuramente il primo, in cui l'intento è quello di proteggere una particolare locazione di memoria da modifiche accidentali, ad esempio per effetto collaterale di una chiamata a funzione. Gli altri due casi, in cui si inibisce la modifica anche della variabile puntatore che contiene l'indirizzo di memoria sono meno frequenti.
Probabilmente il modo migliore per capire l'effetto di una dichiarazione in cui compare il qualificatore const
consiste nel leggere da destra verso sinistra. Ad esempio, nel caso di const int *ptr1
si ha che ptr1 è l'indirizzo (*
) di una locazione di memoria che contiente un valore intero (int
) non modificabile (const
).
A volte aiuta riformulare la dichiarazione secondo uno stile più vicino al linguaggio C per quanto riguarda la posizione dell'asterisco nella dichiarazione di una variabile puntatore. In questo caso modificando int* const ptr2
in int const *ptr2
, possiamo applicare nuovamente il nostro metodo di lettura per comprendere meglio la dichiarazione di ptr2: ptr2 è l'indirizzo (*
) non modificabile (const
) di una locazione di memoria che contiene un valore intero (int
).
Dichiarazione di reference
Anche la dichiarazione di variabili reference con l'uso del qualificatore const
merita alcune considerazioni.
Data la loro particolare funzione di alias, le variabili reference già integrano nella loro funzionalità il concetto di immutabilità, poiché a differenza dei puntatori, esse non possono essere associate ad una locazione di memoria differente dopo la loro prima inizializzazione. Sono di fatto dei puntatori immutabili per definizione, come nel caso di int* const ptr2
.
Tuttavia, il qualificatore const
trova applicazione anche con le variabili reference poiché rende immutabile la locazione di memoria cui la reference è associata. In questo caso, però, poiché è già semanticamente impossibile alterare l'indirizzo di memoria associato alla reference, la posizione del qualificatore nel contesto della dichiarazione è indifferente, come mostrato nel listato seguente:
int main()
{
int a = 111;
const int &ref1 = a; // ref1 è associata ad una locazione di memoria non modificabile.
int const &ref2 = a; // come sopra.
// ref1 = 222; // errore: const inibisce le modifiche al valore di a mediante ref1.
// int& const ref = a; // errore di sintassi, una reference è intrinsecamente 'const',
return 0;
}
Inoltre, si ricorda che l'uso di const reference assume una valenza particolare nel caso di valori temporanei, ad esempio per il tipo di argomento usato dal costruttore di copia di una classe e per la ridefinizione degli operatori del linguaggio.
Valore di ritorno
Anche il valore di ritorno di una funzione può essere qualificato come immutabile. Il caso più ricorrente è quello di funzioni che restituiscono puntatori ad oggetti o stringhe di testo che si intendono per la sola lettura (si pensi, per esempio, ad una funzione di libreria che restituisce il numero di versione del driver di una periferica).
Tuttavia, questo tipo di usi è solitamente legato ad uno stile di programmazione più consono al linguaggio C ed antecedente lo standard C++11, dove la semantica dello spostamento tende a confliggere con l'uso di valori di ritorno costanti.
const vs #define
Nell'ambito della definizione di valori costanti, l'uso di const
si presta bene a sostituire l'uso della direttiva al preprocessore #define per molteplici ragioni:
- Una variabile immutabile, rispetto l'uso di un semplice letterale, ha un tipo e questo consente di irrobustire il controllo statico in fase di compilazione.
- A differenza di una macro, che è definita nello spazio globale, una variabile può avere un ambito di visibilità più ristretto, ad esempio una classe o un namespace, al di fuori del quale essa non è visibile ed il suo nome è riutilizzabile.
- Una variabile, a differenza di un letterale è un'entità referenziabile, e per ragioni che vedremo meglio nel seguito, questa differenza è rilevante per tutti i programmi che si articolano su più file sorgenti e moduli.
Linkage e ottimizzazioni del compilatore
Nel caso della definizione di valori costanti che ricorrono in più translation unit (l'unione di un file .cpp e tutti gli header in esso inclusi), è importante tenere conto di un concetto fondamentale: il linkage.
Il linkage qualifica il livello di visibilità di un'entità (variabile, funzione, etc.) in fase di linking per la risoluzione dei simboli che appaiono nei vari file oggetto prodotti dal compilatore.
Esistono tre livelli: interno ed esterno per le variabili globali e nessuno per quelle locali. Una variabile con linkage interno è visibile globalmente in tutti i contesti dell'unità di traduzione in cui è definita, ma non al di fuori di essa, mentre una variabile con linkage esterno può essere visibile per più translation unit.
Lo standard C++ prescrive che una variabile globale cui venga applicato il qualificatore const
ha linkage interno per default. Questa caratteristica è un fattore abilitante per l'applicazione di ottimizzazioni specifiche per l'uso di valori costanti da parte del compilatore, in particolare l'inlining, cioè la sostituzione di una variable const
con un valore immediato per evitare il costo di una dereferenziazione.
Questo comportamento può essere cambiato mediante l'uso della parola riservata extern
, che rende possibile rendere visibile in fase di linking variabili che sono dichiarate globalmente in uno specifico file .cpp.
Ci si potrebbe chiedere perché sia importante conoscere concetti come linkage ed inilining, se in fin dei conti stiamo parlando di valori destinati a rimanere immutati per l'intera esecuzione del programma. Cosa importa se il compilatore usa una variabile costante o un valore immediato?
La risposta consiste nel fatto che, il linguaggio C++ consente di aggirare il vincolo di immutabilità per le variabili mediante un apposito operatore di casting che esamineremo nelle lezioni successive. Tuttavia, come vedremo nelle lezioni successive, se usato in maniera inopportuna tale operatore può degenerare in ciò che si chiama comportamento indefinito, cioè una condizione in cui lo stato di alcune variabili non è ben definito ed è altamente probabile che ciò determini un errore ingestibile, ad esempio una violazione di accesso alla memoria.