Il Qualificatore const in C++

Concetti Chiave
  • Il qualificatore const in C++ viene utilizzato per dichiarare variabili il cui valore non può essere modificato dopo l'inizializzazione.
  • Le variabili const devono essere inizializzate al momento della dichiarazione.
  • Le variabili const possono essere inizializzate con espressioni complesse, ma non possono essere modificate successivamente.
  • Anche i riferimenti e i puntatori possono essere dichiarati come const, con regole specifiche su cosa può essere modificato.
  • Le variabili constexpr sono costanti che devono essere inizializzate con espressioni valutabili a tempo di compilazione.

Il qualificatore const

A volte vogliamo definire una variabile il cui valore sappiamo già non potrà essere cambiato.

Per esempio, potremmo voler usare una variabile per riferirci alla dimensione di un buffer. Usare una variabile ci rende facile cambiare la dimensione del buffer se decidessimo che la dimensione originale non era quella di cui avevamo bisogno. D'altra parte, vorremmo anche impedire al codice di dare inavvertitamente un nuovo valore alla variabile che usiamo per rappresentare la dimensione del buffer.

Possiamo rendere una variabile immodificabile aggiungendo al tipo della variabile il qualificatore const:

// dimensione del buffer di input
const int dimensione_buffer = 512;

questo codice definisce dimensione_buffer come una costante. Qualsiasi tentativo di assegnare un nuovo valore a dimensione_buffer è un errore:

// ERRORE: tentativo di scrivere su un oggetto const
dimensione_buffer = 512;

Poiché non possiamo cambiare il valore di un oggetto const dopo averlo creato, deve essere necessariamente inizializzato. Come al solito, l'inizializzatore può essere un'espressione arbitrariamente complicata:

// OK: inizializzato a runtime
const int i = ottieni_dimensione();

// OK: inizializzato a tempo di compilazione
const int j = 42;

// ERRORE: k è una const non inizializzata
const int k;

Inizializzazione e const

Come abbiamo osservato molte volte, il tipo di un oggetto definisce le operazioni che possono essere eseguite da quell'oggetto.

Un tipo const può usare la maggior parte, ma non tutte, delle stesse operazioni della sua versione non const. L'unica restrizione è che possiamo usare solo quelle operazioni che non modificano un oggetto.

Quindi, per esempio, possiamo usare un const int in espressioni aritmetiche esattamente nello stesso modo di un int normale, non const. Un const int si converte a bool nello stesso modo di un int normale, e così via.

Tra le operazioni che non cambiano il valore di un oggetto c'è l'inizializzazione, quando usiamo un oggetto per inizializzare un altro oggetto, non importa se uno o entrambi gli oggetti sono const:

int i = 42;

// OK: il valore in i è copiato in ci
const int ci = i;

// OK: il valore in ci è copiato in j
int j = ci;

Sebbene ci sia un const int, il valore in ci è un int. La const-ness di ci conta solo per le operazioni che potrebbero cambiare ci. Quando copiamo ci per inizializzare j, non ci importa che ci sia const. Copiare un oggetto non cambia quell'oggetto. Una volta fatta la copia, il nuovo oggetto non ha ulteriore accesso all'oggetto originale.

In questo, l'assegnazione differisce dall'inizializzazione. Quando assegniamo un oggetto a un altro, stiamo cambiando il valore dell'oggetto di sinistra. Pertanto, non possiamo assegnare a un oggetto const:

ci = 0; // errore: ci è const

Di Default, gli Oggetti const sono locali a un File

Quando un oggetto const è inizializzato da una costante a compile-time, come nella nostra definizione di dimensione_buffer:

// dimensione del buffer di input
const int dimensione_buffer = 512;

il compilatore di solito sostituirà gli usi della variabile con il suo valore corrispondente durante la compilazione. Cioè, il compilatore genererà codice usando il valore 512 nei posti in cui il nostro codice usa dimensione_buffer.

Per sostituire il valore per la variabile, il compilatore deve vedere l'inizializzatore della variabile. Quando dividiamo un programma in più file, ogni file che usa la const deve avere accesso al suo inizializzatore. Per vedere l'inizializzatore, la variabile deve essere definita in ogni file che vuole usare il valore della variabile. Per supportare questo uso, ma evitare definizioni multiple della stessa variabile, le variabili const sono definite come locali al file. Quando definiamo una const con lo stesso nome in più file, è come se avessimo scritto definizioni per variabili separate in ogni file.

A volte abbiamo una variabile const che vogliamo condividere tra più file ma il cui inizializzatore non è un'espressione costante. In questo caso, non vogliamo che il compilatore generi una variabile separata in ogni file. Invece, vogliamo che l'oggetto const si comporti come altre variabili (non const). Vogliamo definire la const in un file, e dichiararla negli altri file che usano quell'oggetto.

Per definire una singola istanza di una variabile const, usiamo la parola chiave extern sia sulla sua definizione che sulle dichiarazioni:

// file_1.cpp definisce e inizializza una const accessibile ad altri file
extern const int dimensione_buffer = funzione();

// file_1.h
// stessa dimensione_buffer definita in file_1.cpp
extern const int dimensione_buffer;

In questo programma, file_1.cpp definisce e inizializza dimensione_buffer. Poiché questa dichiarazione include un inizializzatore, è (come al solito) una definizione. Tuttavia, poiché dimensione_buffer è const, dobbiamo specificare extern per permettere a dimensione_buffer di essere usata in altri file.

La dichiarazione in file_1.h è anche extern. In questo caso, extern significa che dimensione_buffer non è locale a questo file e che la sua definizione avverrà altrove.

Consiglio

Condividere oggetti const tra più file

Per condividere un oggetto const tra più file, è necessario definire la variabile come extern.

Esercizi

  • Quali delle seguenti righe sono legali? Per quelle illegali, spiega perché.

    // Riga 1
    const int buf;
    
    // Riga 2
    int cnt = 0;
    
    // Riga 3
    const int sz = cnt;
    
    // Riga 4
    ++cnt; ++sz;
    

    Soluzione:

    • Riga 1: Non è legale. Un oggetto const deve essere inizializzato al momento della definizione.
    • Riga 2: È legale. cnt è una variabile int normale e può essere inizializzata a 0.
    • Riga 3: È legale. sz è un oggetto const int e viene inizializzato con il valore di cnt. Anche se cnt non è const, il valore di cnt viene copiato in sz al momento della definizione.
    • Riga 4: Non è legale. sz è const, quindi non può essere modificato dopo la sua inizializzazione.

Riferimenti a const

Come con qualsiasi altro oggetto, possiamo legare un riferimento a un oggetto di tipo const. Per farlo usiamo un riferimento a const, che è un riferimento che si riferisce a un tipo const. A differenza di un riferimento ordinario, un riferimento a const non può essere usato per cambiare l'oggetto a cui il riferimento è legato:

const int ci = 1024;

// OK: sia il riferimento che l'oggetto sottostante sono const
const int &r1 = ci;

// ERRORE: r1 è un riferimento a const
r1 = 42;

// ERRORE: riferimento non const a un oggetto const
int &r2 = ci;

Poiché non possiamo assegnare direttamente a ci, non dovremmo nemmeno essere in grado di usare un riferimento per cambiare ci. Pertanto, l'inizializzazione di r2 è un errore. Se questa inizializzazione fosse legale, potremmo usare r2 per cambiare il valore del suo oggetto sottostante.

Definizione

Terminologia: Riferimento const è un riferimento a const

I programmatori C++ tendono ad abbreviare la frase "riferimento a const" come "riferimento const".

Tecnicamente parlando, non ci sono riferimenti const. Un riferimento non è un oggetto, quindi non possiamo rendere un riferimento stesso const. Infatti, poiché non c'è modo di far riferire un riferimento a un oggetto diverso, in un certo senso tutti i riferimenti sono const. Se un riferimento si riferisce a un tipo const o non const influisce su cosa possiamo fare con quel riferimento, non su se possiamo alterare il legame del riferimento stesso.

Inizializzazione e Riferimenti a const

Nella lezione sui riferimenti abbiamo notato che ci sono due eccezioni alla regola che il tipo di un riferimento deve corrispondere al tipo dell'oggetto a cui si riferisce. La prima eccezione è che possiamo inizializzare un riferimento a const da qualsiasi espressione che può essere convertita al tipo del riferimento. In particolare, possiamo legare un riferimento a const a un oggetto non const, un letterale, o un'espressione più generale:

int i = 42;

// possiamo legare un const int& a un oggetto int normale
const int &r1 = i;

// OK: r1 è un riferimento a const
const int &r2 = 42;

// OK: r3 è un riferimento a const
const int &r3 = r1 * 2;

// ERRORE: r4 è un riferimento normale, non const
int &r4 = r * 2;

Il modo più facile per comprendere questa differenza nelle regole di inizializzazione è considerare cosa succede quando leghiamo un riferimento a un oggetto di tipo diverso:

double valore_double = 3.14;
const int &ri = valore_double;

Qui ri si riferisce a un int. Le operazioni su ri saranno operazioni su interi, ma valore_double è un numero in virgola mobile, non un intero. Per assicurare che l'oggetto a cui ri è legato sia un int, il compilatore trasforma questo codice in qualcosa come:

// crea un oggetto temporaneo const int dal double
const int temp = valore_double;

// lega ri a quel temporaneo
const int &ri = temp;

In questo caso, ri è legato a un oggetto temporaneo. Un oggetto temporaneo è un oggetto senza nome creato dal compilatore quando ha bisogno di un posto dove memorizzare un risultato dalla valutazione di un'espressione. I programmatori C++ usano spesso la parola temporaneo come abbreviazione per oggetto temporaneo.

Ora consideriamo cosa potrebbe accadere se questa inizializzazione fosse permessa ma ri non fosse const. Se ri non fosse const, potremmo assegnare a ri. Così facendo cambieremmo l'oggetto a cui ri è legato. Quell'oggetto è un temporaneo, non valore_double. Il programmatore che ha fatto riferire ri a valore_double probabilmente si aspetterebbe che assegnare a ri cambierebbe valore_double. Dopo tutto, perché assegnare a ri a meno che l'intento non sia di cambiare l'oggetto a cui ri è legato? Poiché legare un riferimento a un temporaneo è quasi certamente non ciò che il programmatore intendeva, il linguaggio lo rende illegale.

Un riferimento a const può riferirsi a un Oggetto che non è const

È importante rendersi conto che un riferimento a const restringe solo ciò che possiamo fare attraverso quel riferimento. Legare un riferimento a const a un oggetto non dice nulla sul fatto che l'oggetto sottostante stesso sia const. Poiché l'oggetto sottostante potrebbe essere non const, potrebbe essere cambiato con altri mezzi:

int i = 42;

// r1 legato a i
int &r1 = i;

// r2 anche legato a i; ma non può essere usato per cambiare i
const int &r2 = i;

// r1 non è const; i è ora 0
r1 = 0;

// ERRORE: r2 è un riferimento a const
r2 = 0;

Legare r2 all'int (non const) i è legale. Tuttavia, non possiamo usare r2 per cambiare i. Anche così, il valore in i potrebbe ancora cambiare. Possiamo cambiare i modificandolo direttamente, o assegnando a un altro riferimento legato a i, come r1.

Puntatori e const

Come con i riferimenti, possiamo definire puntatori che puntano a tipi const o non const. Come un riferimento a const, un puntatore a const non può essere usato per cambiare l'oggetto a cui il puntatore punta. Possiamo memorizzare l'indirizzo di un oggetto const solo in un puntatore a const:

// pi è const; il suo valore non può essere cambiato
const double pi = 3.14;

// ERRORE: punt è un puntatore normale
double *punt = π

// OK: punt_const può puntare a un double che è const
const double *punt_const = π

// ERRORE: non si può assegnare a *punt_const
*punt_const = 42;

Nella lezione sui puntatori abbiamo notato che ci sono due eccezioni alla regola che i tipi di un puntatore e dell'oggetto a cui punta devono corrispondere. La prima eccezione è che possiamo usare un puntatore a const per puntare a un oggetto non const:

// valore_double è un double; il suo valore può essere cambiato
double valore_double = 3.14;

// OK: ma non può cambiare valore_double attraverso punt_const
punt_const = &valore_double;

Come un riferimento a const, un puntatore a const non dice nulla sul fatto che l'oggetto a cui il puntatore punta sia const. Definire un puntatore come un puntatore a const influisce solo su ciò che possiamo fare con il puntatore. È importante ricordare che non c'è garanzia che un oggetto puntato da un puntatore a const non cambierà.

Consiglio

Puntatori a const e Riferimenti a const

Può essere utile pensare ai puntatori e ai riferimenti a const come puntatori o riferimenti "che pensano di puntare o riferirsi a const."

Puntatori const

A differenza dei riferimenti, i puntatori sono oggetti. Quindi, come con qualsiasi altro tipo di oggetto, possiamo avere un puntatore che è esso stesso const. Come qualsiasi altro oggetto const, un puntatore const deve essere inizializzato, e una volta inizializzato, il suo valore (cioè, l'indirizzo che contiene) non può essere cambiato. Indichiamo che il puntatore è const mettendo const dopo il *. Questo posizionamento indica che è il puntatore, non il tipo puntato, che è const:

int numero_errore = 0;

// errore_corrente punterà sempre a numero_errore
int *const errore_corrente = &numero_errore;

const double pi = 3.14159;

// punt_pi è un puntatore const a un oggetto const
const double *const punt_pi = π

Come abbiamo visto nella lezione sulle dichiarazioni di tipi composti, il modo più facile per comprendere queste dichiarazioni è leggerle da destra a sinistra.

In questo caso, il simbolo più vicino a errore_corrente è const, che significa che errore_corrente stesso sarà un oggetto const. Il tipo di quell'oggetto è formato dal resto del dichiaratore. Il simbolo successivo nel dichiaratore è l'asterisco *, che significa che errore_corrente è un puntatore const. Infine, il tipo base della dichiarazione completa il tipo di errore_corrente, che è un puntatore const a un oggetto di tipo int. Similmente, punt_pi è un puntatore const a un oggetto di tipo const double.

Il fatto che un puntatore sia esso stesso const non dice nulla sul fatto che possiamo usare il puntatore per cambiare l'oggetto sottostante. Se possiamo cambiare quell'oggetto dipende interamente dal tipo a cui il puntatore punta. Per esempio, punt_pi è un puntatore const a const. Né il valore dell'oggetto indirizzato da punt_pi né l'indirizzo memorizzato in punt_pi possono essere cambiati. D'altra parte, errore_corrente indirizza un int normale, non const. Possiamo usare errore_corrente per cambiare il valore di numero_errore:

// errore: punt_pi è un puntatore a const
*punt_pi = 2.72;
// se l'oggetto a cui errore_corrente punta (cioè, numero_errore) è diverso da zero
if (*errore_corrente) {
    gestore_errore();

    // OK: resetta il valore dell'oggetto a cui errore_corrente è legato
    *errore_corrente = 0;
}

Esercizi

  • Quali delle seguenti inizializzazioni sono legali? Spiega perché.

    int i = -1, &r = 0; 
    

    Non è legale. Un riferimento deve essere inizializzato con un oggetto esistente, non con un valore letterale.

    int i;
    int *const p2 = &i; 
    

    È legale. p2 è un puntatore const a int e viene inizializzato con l'indirizzo di i.

    const int i = -1, &r = 0; 
    

    È legale. i è un const int e deve essere inizializzato. r è un riferimento a const int e può essere inizializzato con un valore letterale.

    int i = -1;
    const int *const p3 = &i; 
    

    È legale. p3 è un puntatore const a const int e viene inizializzato con l'indirizzo di i. Anche se i non è const, possiamo comunque puntare a esso con un puntatore a const.

    const int x = 42;
    int *p1 = &x; 
    

    Non è legale. Non possiamo assegnare l'indirizzo di un const int a un puntatore a int non const, poiché ciò permetterebbe di modificare un oggetto const attraverso il puntatore.

  • Spiega le seguenti definizioni. Identifica quelle illegali.

    int i; *const cp;
    

    Non è legale. cp è un puntatore const e deve essere inizializzato al momento della definizione.

    int *p1, *const p2;
    

    Non è legale. p2 è un puntatore const e deve essere inizializzato al momento della definizione.

    const int ic, &r = ic;
    

    Non è legale. ic è un const int e deve essere inizializzato al momento della definizione.

    const int *const p3;
    

    Non è legale. p3 è un puntatore const a const int e deve essere inizializzato al momento della definizione.

    const int *p;
    

    È legale. p è un puntatore a const int e può essere inizializzato in un secondo momento.

  • Date le seguenti definizioni:

    int i = 42;
    const int ci = i;
    int *p1 = &i;
    const int *p2 = &ci;
    const int *const p3 = &ci;
    

    Quali delle seguenti assegnazioni sono legali? Spiega perché.

    i = ci; 
    

    È legale. Stiamo copiando il valore di ci in i. La const-ness di ci non conta perché stiamo solo leggendo il suo valore.

    p1 = p3; 
    

    Non è legale. p3 è un puntatore const a const int, mentre p1 è un puntatore a int. Non possiamo assegnare un puntatore a const int a un puntatore a int non const.

    p1 = &ci; 
    

    Non è legale. ci è un const int, quindi non possiamo assegnare il suo indirizzo a un puntatore a int non const.

    p3 = $ci; 
    

    Non è legale. p3 è un puntatore const, quindi non possiamo cambiare l'indirizzo a cui punta dopo la sua inizializzazione.

    p2 = p1;
    

    È legale. p1 è un puntatore a int, e possiamo assegnarlo a p2, che è un puntatore a const int. Questo è permesso perché stiamo passando da un tipo meno restrittivo (non const) a uno più restrittivo (const).

    ic = *p3;
    

    Non è legale. ic è un const int e non può essere modificato dopo la sua inizializzazione.

const di alto livello

Come abbiamo visto, un puntatore è un oggetto che può puntare a un oggetto diverso. Di conseguenza, possiamo parlare indipendentemente del fatto che un puntatore sia const e se gli oggetti a cui può puntare sono const. Usiamo il termine const di alto livello per indicare che il puntatore stesso è const. Quando un puntatore può puntare a un oggetto const, ci riferiamo a quella const come const di basso livello.

Più in generale, const di alto livello indica che un oggetto stesso è const. const di alto livello può apparire in qualsiasi tipo di oggetto, cioè, uno dei tipi aritmetici built-in, un tipo classe, o un tipo puntatore. const di basso livello appare nel tipo base di tipi composti come puntatori o riferimenti. Si noti che i tipi puntatore, a differenza della maggior parte degli altri tipi, possono avere sia const di alto livello che di basso livello indipendentemente:

int i = 0;

// non possiamo cambiare il valore di p1; const è di alto livello
int *const p1 = &i;

// non possiamo cambiare ci; const è di alto livello
const int ci = 42;

// possiamo cambiare p2; const è di basso livello
const int *p2 = &ci;

// const più a destra è di alto livello, quella a sinistra no
const int *const p3 = p2;

// const nei tipi riferimento è sempre di basso livello
const int &r = ci;

La distinzione tra alto livello e basso livello conta quando copiamo un oggetto. Quando copiamo un oggetto, le const di alto livello sono ignorate:

// OK: copiare il valore di ci;
// const di alto livello in ci è ignorata
i = ci;

// OK: il tipo puntato corrisponde;
// const di alto livello in p3 è ignorata
p2 = p3;

Copiare un oggetto non cambia l'oggetto copiato. Di conseguenza, è irrilevante se l'oggetto da cui si copia o in cui si copia è const.

D'altra parte, const di basso livello non è mai ignorata. Quando copiamo un oggetto, entrambi gli oggetti devono avere la stessa qualificazione const di basso livello o deve esserci una conversione tra i tipi dei due oggetti. In generale, possiamo convertire un non const in const ma non il contrario:

// ERRORE: p3 ha una const di basso livello ma p no
int *p = p3;

// OK: p2 ha la stessa qualificazione const di basso livello di p3
p2 = p3;

// OK: possiamo convertire int* a const int*
p2 = &i;

// ERRORE: non si può legare un int& ordinario a un oggetto const int
int &r = ci;

// OK: si può legare const int& a un int normale
const int &r2 = i;

p3 ha sia una const di alto livello che di basso livello. Quando copiamo p3, possiamo ignorare la sua const di alto livello ma non il fatto che punta a un tipo const. Quindi, non possiamo usare p3 per inizializzare p, che punta a un int normale (non const). D'altra parte, possiamo assegnare p3 a p2. Entrambi i puntatori hanno lo stesso tipo (const di basso livello). Il fatto che p3 sia un puntatore const (cioè, che abbia una const di alto livello) non conta.

Esercizi

  • Per ciascuna delle seguenti dichiarazioni indica se l'oggetto dichiarato ha const di alto livello o di basso livello.

    const int v2 = 0; int v1 = v2;
    int *p1 = &v1, &r1 = v1;
    const int *p2 = &v2, *const p3 = &i, &r2 = v2;
    
    • v2: const di alto livello (è un const int)
    • v1: nessuna const (è un int normale)
    • p1: nessuna const (è un puntatore a int normale)
    • r1: nessuna const (è un riferimento a int normale)
    • p2: const di basso livello (è un puntatore a const int)
    • p3: const di alto livello e di basso livello (è un puntatore const a const int)
    • r2: const di basso livello (è un riferimento a const int)
  • Date le dichiarazioni nell'esercizio precedente determina se le seguenti assegnazioni sono legali. Spiega come la const di alto o basso livello si applica in ogni caso.

    r1 = v2;
    p1 = p2;
    p2 = p1;
    p1 = p3;
    p2 = p3;
    
    • r1 = v2;: È legale. Stiamo copiando il valore di v2 in v1 attraverso il riferimento r1. La const di alto livello di v2 non conta perché stiamo solo leggendo il suo valore.
    • p1 = p2;: Non è legale. p2 è un puntatore a const int (const di basso livello), mentre p1 è un puntatore a int normale. Non possiamo assegnare un puntatore a const int a un puntatore a int non const.
    • p2 = p1;: È legale. p1 è un puntatore a int normale, e possiamo assegnarlo a p2, che è un puntatore a const int. Questo è permesso perché stiamo passando da un tipo meno restrittivo (non const) a uno più restrittivo (const).
    • p1 = p3;: Non è legale. p3 è un puntatore const a const int, mentre p1 è un puntatore a int normale. Non possiamo assegnare un puntatore a const int a un puntatore a int non const, e inoltre non possiamo cambiare l'indirizzo a cui punta p3 perché è const.
    • p2 = p3;: È legale. p3 è un puntatore const a const int, e p2 è un puntatore a const int. La const di alto livello di p3 non conta perché stiamo solo leggendo il suo valore.

constexpr ed Espressioni Costanti

Un'espressione costante è un'espressione il cui valore non può cambiare e che può essere valutata a tempo di compilazione.

Un letterale è un'espressione costante. Un oggetto const che è inizializzato da un'espressione costante è anche un'espressione costante. Come vedremo, ci sono diversi contesti nel linguaggio che richiedono espressioni costanti.

Se un dato oggetto (o espressione) è un'espressione costante dipende dai tipi e dagli inizializzatori. Per esempio:

// file_massimi è un'espressione costante
const int file_massimi = 20;

// limite è un'espressione costante
const int limite = file_massimi + 1;

// dimensione_staff non è un'espressione costante
int dimensione_staff = 27;

// sz non è un'espressione costante
const int sz = ottieni_dimensione();

Sebbene dimensione_staff sia inizializzato da un letterale, non è un'espressione costante perché è un int normale, non un const int. D'altra parte, anche se sz è const, il valore del suo inizializzatore non è noto fino a tempo di esecuzione. Quindi, sz non è un'espressione costante.

Variabili constexpr

In un progetto software di grandi dimensioni, può essere difficile determinare (con certezza) che un inizializzatore è un'espressione costante.

Potremmo definire una variabile const con un inizializzatore che pensiamo sia un'espressione costante. Tuttavia, quando usiamo quella variabile in un contesto che richiede un'espressione costante potremmo scoprire che l'inizializzatore non era un'espressione costante. In generale, la definizione di un oggetto e il suo uso in tale contesto possono essere ampiamente separati.

Sotto lo standard C++11, possiamo chiedere al compilatore di verificare che una variabile sia un'espressione costante dichiarando la variabile in una dichiarazione constexpr. Le variabili dichiarate come constexpr sono implicitamente const e devono essere inizializzate da espressioni costanti:

// 20 è un'espressione costante
constexpr int mf = 20;

// mf + 1 è un'espressione costante
constexpr int limite = mf + 1;

// OK solo se dimensione è una funzione constexpr
constexpr int sz = dimensione();

Sebbene non possiamo usare una funzione ordinaria come inizializzatore per una variabile constexpr, vedremo nelle prossime lezioni che lo standard C++11 ci permette di definire certe funzioni come constexpr. Tali funzioni devono essere abbastanza semplici da permettere al compilatore di valutarle a tempo di compilazione. Possiamo usare funzioni constexpr nell'inizializzatore di una variabile constexpr.

Consiglio

Quando usare constexpr

Generalmente, è una buona idea usare constexpr per variabili che si intende usare come espressioni costanti.

Tipi Letterali

Poiché un'espressione costante è un'espressione che può essere valutata a tempo di compilazione, ci sono limiti sui tipi che possiamo usare in una dichiarazione constexpr. I tipi che possiamo usare in una constexpr sono noti come "tipi letterali" perché sono abbastanza semplici da avere valori letterali.

Dei tipi che abbiamo usato finora, i tipi aritmetici, riferimento e puntatore sono tipi letterali. La nostra classe ArticoloVendite e i tipi libreria I/O e string non sono tipi letterali. Quindi, non possiamo definire variabili di questi tipi come constexpr. Vedremo altri tipi di tipi letterali nelle prossime lezioni.

Sebbene possiamo definire sia puntatori che riferimenti come constexpr, gli oggetti che usiamo per inizializzarli sono strettamente limitati. Possiamo inizializzare un puntatore constexpr dal letterale nullptr o dal letterale (cioè, espressione costante) 0. Possiamo anche puntare a (o legare a) un oggetto che rimane a un indirizzo fisso.

Per ragioni che tratteremo in futuro, le variabili definite dentro una funzione ordinariamente non sono memorizzate a un indirizzo fisso. Quindi, non possiamo usare un puntatore constexpr per puntare a tali variabili. D'altra parte, l'indirizzo di un oggetto definito fuori da qualsiasi funzione è un'espressione costante, e quindi può essere usato per inizializzare un puntatore constexpr. Vedremo che le funzioni possono definire variabili che esistono attraverso le chiamate a quella funzione. Come un oggetto definito fuori da qualsiasi funzione, questi oggetti locali speciali hanno anche indirizzi fissi. Pertanto, un riferimento constexpr può essere legato a tali variabili, e un puntatore constexpr le può indirizzare.

Puntatori e constexpr

È importante capire che quando definiamo un puntatore in una dichiarazione constexpr, lo specificatore constexpr si applica al puntatore, non al tipo a cui il puntatore punta:

// p è un puntatore a un const int
const int *p = nullptr;

// q è un puntatore const a int
constexpr int *q = nullptr;

Nonostante le apparenze, i tipi di p e q sono abbastanza diversi; p è un puntatore a const, mentre q è un puntatore costante. La differenza è una conseguenza del fatto che constexpr impone una const di alto livello sugli oggetti che definisce.

Come qualsiasi altro puntatore costante, un puntatore constexpr può puntare a un tipo const o non const:

// punt_null è un puntatore costante a int che è nullptr
constexpr int *punt_null = nullptr;

int j = 0;

// il tipo di i è const int
constexpr int i = 42;

// i e j devono essere definiti fuori da qualsiasi funzione
// p è un puntatore costante al const int i
constexpr const int *p = &i;

// p1 è un puntatore costante all'int j
constexpr int *p1 = &j;