L’Incredibile Const Riferimento Che non È Const
Mentre si lavora sul NamedType libreria mi sono imbattuto in una situazione che mi ha lasciato sbalordito nello smarrimento: const riferimento che consente la modifica dell’oggetto a cui si riferisce. Senza a const_cast
. Senza a mutable
. Senza niente nella manica.
Come può essere? E come applicare const
in quel riferimento const?
Un riferimento const che non è const
Ecco una descrizione di una situazione in cui viene visualizzato questo strano riferimento. Considera la seguente classe wrapper:
template<typename T>class Wrapper{public: Wrapper(T const& value) : value_(value) {} T const& get() const { return value_; } private: T value_;};
Questa classe Wrapper
memorizza un valore di tipo T
e dà accesso ad esso tramite il metodo get()
(indipendentemente da questo, se ti stai chiedendo se “get” è un buon nome, potresti voler leggere questa discussione dettagliata). La parte importante è che il metodo get()
fornisce solo un accesso di sola lettura al valore memorizzato, perché restituisce un riferimento const.
Per verificarlo, istanziiamo il modello con int
, ad esempio. Il tentativo di modificare il valore memorizzato tramite il metodo get()
non riesce a compilare (provare a fare clic sul pulsante Esegui sotto).
Che va bene, perché vogliamo che get()
sia di sola lettura.
Ma ora istanziiamo Wrapper
con int&
. Un caso d’uso per questo potrebbe essere quello di evitare una copia o tenere traccia di potenziali modifiche di a
, ad esempio. Ora proviamo a modificare il valore memorizzato attraverso il riferimento const restituito da get()
:
E se premi il pulsante RUN vedrai che comp si compila! E stampa il valore modificato attraverso il riferimento const, come se non fosse successo nulla di speciale.
Non è sconcertante? Non senti l’impressione rassicurante della sicurezza fornita da const
che crolla intorno a noi? Di chi possiamo fidarci?
Proviamo a capire cosa sta succedendo con questo riferimento.
Riferimento Const o riferimento a const?
Il nocciolo della questione è che il nostro riferimento a è un riferimento const, ma non un riferimento a const.
Per capire cosa significa, scomponiamo passo dopo passo ciò che sta accadendo nell’istanziazione del modello.
Il metodo get()
restituisce un const T&
, con T
proveniente da template T
. Nel nostro secondo caso, T
è int&
, quindi const T&
è const (int&) &
.
Diamo un’occhiata più da vicino a questo const (int&)
. int&
è un riferimento che fa riferimento a un int
ed è consentito modificarlo int
. Ma const (int&)
è un riferimento int&
che è const
, il che significa che il riferimento stesso non può essere modificato.
Cosa significa modificare un riferimento, per cominciare? In teoria, significherebbe farlo riferimento a un altro oggetto, o non fare riferimento a nulla. Ma entrambe queste possibilità sono illegali in C++: un riferimento non può essere riavviato. Si riferisce allo stesso oggetto durante l’intero corso della sua vita.
Quindi essere const
non dice molto per un riferimento, dal momento che sono sempre const
, dal momento che non possono rebindare. Ciò implica che const (int&)
è effettivamente lo stesso tipo di int&
.
Che ci lascia con get()
che restituisce (int&) &
. E secondo le regole dei riferimenti che collassano, questo è int&
.
Quindi get
restituisce un int&
. La sua interfaccia dice T const&
, ma è in realtà un int&
. Codice non proprio espressivo, vero?
Come rendere const reference un riferimento a const
Ora che abbiamo superato il primo passo per comprendere il problema, come possiamo risolverlo? Cioè, come possiamo fare get()
restituire un riferimento al valore memorizzato che non consente di modificarlo?
Un modo per farlo è inserire esplicitamente un const
all’interno del riferimento. Possiamo farlo togliendo il riferimento, aggiungendo un const
e rimettendo il riferimento. Per rimuovere il riferimento, usiamo std::remove_reference
in C++11, o il più conveniente std::remove_reference_t
in C++14:
Come si può vedere se si fa clic su Esegui, l’errore di compilazione previsto appare ora quando si tenta di modificare il valore memorizzato in Wrapper
.
Penso che sia utile capire cosa sta succedendo con questo riferimento, perché questo è un codice che sembra sicuro, ma in realtà non lo è, e questo è il tipo più pericoloso.
Puoi giocare con il codice nei campi da gioco sopra incorporati nella pagina. E un grande ringraziamento all’utente stack overflow rakete1111 per avermi aiutato a capire le ragioni di questo problema.
Potrebbe piacerti anche
- L’analisi più fastidiosa: come individuarla e risolverla rapidamente