Norma C++

Const Correctness

Che cos’è “const correctness”?

Una buona cosa. Significa usare la parola chiave const per impedire che gli oggetti const vengano mutati.

Per esempio, se si voleva creare una funzione f() che ha accettato un std::string, più si vuole promessa callersnot per cambiare il numero del chiamante std::string che viene passato a f(), si può avere f() ricevere i suoi std::stringparametro…

  • void f1(const std::string& s); // Passaggio per riferimento-per-const
  • void f2(const std::string* sptr); // Passare da un puntatore-a-const
  • void f3(std::string s); // Passaggio per valore

Nel passaggio da un riferimento-a-const e passare da un puntatore-a-const casi, qualsiasi tentativo di modificare il chiamantestd::string all’interno di f() funzioni dovrebbe essere segnalato dal compilatore come un errore in fase di compilazione. Questo controllo viene eseguito interamente in fase di compilazione: non vi è spazio di runtime o costi di velocità per const. Nel caso pass by value (f3()), la funzione chiamata ottiene una copia di std::stringdel chiamante. Ciò significa che f3() può cambiare il suo localcopy, ma la copia viene distrutta quando f3() restituisce. In particolare f3() non è possibile modificare l’oggetto std::stringdel chiamante.

Come esempio opposto, supponiamo di voler creare una funzione g() che ha accettato un std::string, ma vuoi far sapere ai chiamanti che g() potrebbe cambiare l’oggetto std::string del chiamante. In questo caso si può avere g() ricevere i suoistd::string parametro…

  • void g1(std::string& s); // Passaggio per riferimento-per-non-const
  • void g2(std::string* sptr); // Passare da un puntatore-a-non-const

La mancanza di const in queste funzioni indica al compilatore che essi sono autorizzati, per (ma non necessario) cambiare thecaller s std::string oggetto. Quindi possono passare il loro std::string a una qualsiasi delle funzioni f(), ma solo f3() (quella che riceve il suo parametro “per valore”) può passare il suo std::string a g1()o g2(). Se f1() o f2()è necessario chiamare una funzione g(), una copia locale dell’oggetto std::string deve essere passata alla funzione g(); il parametro a f1() o f2() non può essere passato direttamente a una funzione g(). Ad esempio.,

void g1(std::string& s);void f1(const std::string& s){ g1(s); // Compile-time Error since s is const std::string localCopy = s; g1(localCopy); // Okay since localCopy is not const}

Naturalmente nel caso precedente, tutte le modifiche apportate da g1() vengono apportate all’oggetto localCopyche è locale a f1(). In particolare, non verranno apportate modifiche al parametro constpassato con riferimento a f1().

In che modo la “correttezza const” è correlata alla sicurezza di tipo ordinario?

Dichiarare la const-ness di un parametro è solo un’altra forma di sicurezza del tipo.

Se trovi che la sicurezza di tipo ordinario ti aiuta a ottenere sistemi corretti (lo fa, specialmente nei sistemi di grandi dimensioni), troverai ancheconst la correttezza aiuta.

Il vantaggio della correttezza const è che ti impedisce di modificare inavvertitamente qualcosa che non ti aspettavi sarebbe stato modificato. Finisci per dover decorare il tuo codice con alcune sequenze di tasti extra (la parola chiave const), con il vantaggio che stai dicendo al compilatore e ad altri programmatori qualche ulteriore pezzo di importanti informazioni semantiche— informazioni che il compilatore usa per prevenire errori e altri programmatori usano come documentazione.

Concettualmente si può immaginare che const std::string, ad esempio, sia una classe diversa dall’ordinariastd::string, poiché la variante const manca concettualmente le varie operazioni mutative disponibili nella variante non const. Ad esempio, è possibile immaginare concettualmente che un const std::string semplicemente non abbia un operatore di assegnazione+= o altre operazioni mutative.

Dovrei provare a correggere le cose “prima ” o”dopo”?

All’inizio.

Back-patching const risultati di correttezza in un effetto palla di neve: ogni const si aggiunge ” qui “richiede altri quattro da aggiungere” là.”

Aggiungi const presto e spesso.

Cosa significa “const X * p”?

Significa p punta a un oggetto di classe X, ma p non può essere usato per cambiare quell’oggetto X (naturalmente p potrebbe anche essere NULL).

Leggilo da destra a sinistra: “p è un puntatore a una X costante.”

Ad esempio, se la classe X ha una funzione membro const come inspect() const, va bene direp->inspect(). Ma se la classe X ha una funzione membro nonconst chiamata mutate(), è anerror se dici p->mutate().

Significativamente, questo errore viene rilevato dal compilatore in fase di compilazione — non vengono eseguiti test di runtime. Ciò significa che constnon rallenta il programma e non richiede di scrivere casi di test aggiuntivi per controllare le cose in fase di runtime: il compilatore esegue il lavoro in fase di compilazione.

Qual è la differenza tra “const X * p”, “X* const p”e” const X* const p”?

Leggi le dichiarazioni del puntatore da destra a sinistra.

  • const X* p significa ” p punta a un X che è const“: l’oggetto X non può essere modificato tramitep.
  • X* const p significa ” p è un puntatore const a un X che non èconst“: non è possibile modificare il puntatore pstesso, ma è possibile modificare l’oggetto X tramite p.
  • const X* const p significa “p è un puntatore const a un X che è const“: non è possibile modificare il puntatore pstesso, né è possibile modificare l’oggetto X tramite p.

E, oh sì, ho detto di leggere le dichiarazioni del puntatore da destra a sinistra?

Cosa significa “const X& x”?

Significa x alias un oggetto X, ma non è possibile modificare l’oggetto Xtramite x.

Leggilo da destra a sinistra: “x è un riferimento a un X che è const.”

Ad esempio, se la classe X ha una funzione membro const come inspect() const, va bene direx.inspect(). Ma se la classe X ha una funzione membro nonconst chiamata mutate(), è un errorese dici x.mutate().

Questo è interamente simmetrico con i puntatori a const, incluso il fatto che il compilatore esegue tutto il controllo in fase di compilazione, il che significa che const non rallenta il programma e non richiede di scrivere casi di test aggiuntivi per controllare le cose in fase di runtime.

Cosa significano” X const& x “e” X const* p”?

X const& x equivale a const X& x e X const* xequivale aconst X* x.

Alcune persone preferiscono lo stile const-on-the-right, chiamandolo “coerente const” o, usando un termine coniato da Simon Brand, “East const.”In effetti lo stile” East const “può essere più coerente dell’alternativa: lo stile” East const ” mette sempre il const a destra di ciò che costituisce, mentre l’altro stile a volte mette il const a sinistrae talvolta a destra (per le dichiarazioni del puntatore const e le funzioni membro const).

Con lo stile “East const“, una variabile locale che è const è definita con consta destra:int const a = 42;. Allo stesso modo una variabile static che è const è definita come static double const x = 3.14;.Fondamentalmente ogni const finisce a destra della cosa che costituisce, incluso il const che deve essere a destra: const dichiarazioni di puntatore e con una funzione membro const.

Lo stile “East const” è anche meno confuso se usato con gli alias di tipo: perché foo e bar hanno tipi diversi qui?

using X_ptr = X*;const X_ptr foo;const X* bar;

Utilizzando il “Est const” stile rende questo più chiaro:

using X_ptr = X*;X_ptr const foo;X* const foobar;X const* bar;

Qui è più chiaro che foo e foobar sono dello stesso tipo e che bar è un tipo diverso.

Lo stile “East const” è anche più coerente con le dichiarazioni del puntatore. Contrasto lo stile tradizionale:

const X** foo;const X* const* bar;const X* const* const baz;

con lo stile” East const

X const** foo;X const* const* bar;X const* const* const baz;

Nonostante questi vantaggi, lo stile const-on-the-right non è ancora popolare, quindi il codice legacy tende ad avere lo stile tradizionale.

“X& const x” ha senso?

No, è una sciocchezza.

Per scoprire cosa significa la dichiarazione di cui sopra, leggila da destra a sinistra: “x è un riferimento const a X“. Ma questo è ridondante: i riferimenti sono sempre const, nel senso che non è mai possibile reimpostare areference per farlo riferimento a un oggetto diverso. Mai. Con o senza const.

In altre parole, “X& const x” è funzionalmente equivalente a “X& x“. Dal momento che non stai guadagnando nulla aggiungendoconst dopo &, non dovresti aggiungerlo: confonderà le persone — const farà pensare ad alcune persone che X è const, come se avessi detto “const X& x“.

Che cos’è una “funzione membro const”?

Una funzione membro che ispeziona (piuttosto che muta) il suo oggetto.

Una funzione membro const è indicata da un suffisso const subito dopo l’elenco dei parametri della funzione membro. Memberfunctions con un suffisso const sono chiamati “const funzioni membro ” o ” ispettori.”Le funzioni membro senza suffissoconst sono chiamate” funzioni membro nonconst “o” mutatori.”

class Fred {public: void inspect() const; // This member promises NOT to change *this void mutate(); // This member function might change *this};void userCode(Fred& changeable, const Fred& unchangeable){ changeable.inspect(); // Okay: doesn't change a changeable object changeable.mutate(); // Okay: changes a changeable object unchangeable.inspect(); // Okay: doesn't change an unchangeable object unchangeable.mutate(); // ERROR: attempt to change unchangeable object}

Il tentativo di chiamare unchangeable.mutate() è un errore rilevato in fase di compilazione. Non c’è spazio di runtime o speedpenalty per const e non è necessario scrivere casi di test per controllarlo in fase di runtime.

La funzione membro finale const su inspect() deve essere utilizzata per indicare che il metodo non cambierà lo stato abstract (visibile dal client) dell’oggetto. Questo è leggermente diverso dal dire che il metodo non cambierà i “bit grezzi” di theobject struct. I compilatori C++ non sono autorizzati a prendere l’interpretazione “bitwise” a meno che non possano risolvere il problema di thealiasing, che normalmente non può essere risolto (cioè, potrebbe esistere un alias nonconst che potrebbe modificare lo stato dell’oggetto). Un’altra (importante) intuizione da questo problema di aliasing: puntare a un oggetto con un puntatore a – const non garantisce che l’oggetto non cambierà; promette semplicemente che l’oggetto non cambierà tramite quel puntatore.

Qual è la relazione tra una funzione return-by-reference e una funzione membro const?

Se si desidera restituire un membro dell’oggetto this per riferimento da un metodo inspector, è necessario restituirlo utilizzando reference-to-const (const X& inspect() const) o per valore (X inspect() const).

class Person {public: const std::string& name_good() const; // Right: the caller can't change the Person's name std::string& name_evil() const; // Wrong: the caller can change the Person's name int age() const; // Also right: the caller can't change the Person's age // ...};void myCode(const Person& p) // myCode() promises not to change the Person object...{ p.name_evil() = "Igor"; // But myCode() changed it anyway!!}

La buona notizia è che il compilatore ti prenderà spesso se ti sbagli. In particolare, se accidentalmente restituisci un membro del tuo oggetto this con riferimento nonconst, come in Person::name_evil() sopra, il compilatorespesso lo rileverà e ti darà un errore in fase di compilazione durante la compilazione delle interiora di, in questo caso,Person::name_evil().

La cattiva notizia è che il compilatore non ti prenderà sempre: ci sono alcuni casi in cui il compilatore semplicemente non ti darà un messaggio di errore in fase di compilazione.

Traduzione: devi pensare. Se questo ti spaventa, trova un’altra linea di lavoro; “pensare” non è una parola di quattro lettere.

Ricorda la “filosofia const” diffusa in questa sezione: una funzione membro const non deve cambiare (o consentire a un chiamante di cambiare) lo stato logico dell’oggetto this (AKA stato astratto AKA meaningwisestate). Pensa a cosa significa un oggetto, non a come è implementato internamente. L’età e il nome di una persona sono logicamenteparte della persona, ma il vicino e il datore di lavoro della persona non lo sono. Un metodo inspector che restituisce parte dello stato logico / astratto / significante dell’oggetto this non deve restituire un puntatore (o un riferimento) nonconst a tale parte, indipendentemente dal fatto che tale parte sia implementata internamente come membro diretto dei dati fisicamente incorporato all’interno dell’oggettothis o in altro modo.

Qual è l’accordo con “const-overloading”?

const sovraccarico consente di raggiungere const correttezza.

const l’overload è quando si dispone di un metodo inspector e di un metodo mutatorcon lo stesso nome e lo stesso numero di e tipi di parametri. I due metodi distinti differiscono solo in quanto theinspector è conste il mutatore non èconst.

L’uso più comune di const sovraccarico è con l’operatore pedice. In genere dovresti provare a utilizzare uno deimodelli contenitore standard, come std::vector, ma se hai bisogno di creare la tua classe con un subscriptoperator, ecco la regola empirica: gli operatori di pedice spesso vengono in coppia.

class Fred { /*...*/ };class MyFredList {public: const Fred& operator (unsigned index) const; // Subscript operators often come in pairs Fred& operator (unsigned index); // Subscript operators often come in pairs // ...};

L’operatore pedice constrestituisce un riferimento const, quindi il compilatore impedirà ai chiamanti di modificare/modificare inavvertitamente Fred. L’operatore pedice nonconst restituisce un riferimento nonconst, che è il tuo modo di dire ai chiamanti (e al compilatore) che i chiamanti sono autorizzati a modificare l’oggetto Fred.

Quando un utente della classe MyFredListchiama l’operatore pedice, il compilatore seleziona quale sovraccarico chiamare basedon la costanza del loro MyFredList. Se il chiamante ha un MyFredList a o MyFredList& a, allora a chiamerà l’operatore pedice nonconst e il chiamante finirà con un riferimento nonconst a Fred:

Ad esempio, supponiamo che class Fred abbia un inspector-method inspect() const e un mutator-method mutate():

void f(MyFredList& a) // The MyFredList is non-const{ // Okay to call methods that inspect (look but not mutate/change) the Fred at a: Fred x = a; // Doesn't change to the Fred at a: merely makes a copy of that Fred a.inspect(); // Doesn't change to the Fred at a: inspect() const is an inspector-method // Okay to call methods that DO change the Fred at a: Fred y; a = y; // Changes the Fred at a a.mutate(); // Changes the Fred at a: mutate() is a mutator-method}

Tuttavia se il chiamante ha un const MyFredList a o const MyFredList& a, allora a chiamerà il const subscriptoperator, e il chiamante finirà con un const riferimento a un Fred. Ciò consente al chiamante di ispezionare Freda a, ma impedisce al chiamante di mutare/cambiare inavvertitamente Fred a a.

void f(const MyFredList& a) // The MyFredList is const{ // Okay to call methods that DON'T change the Fred at a: Fred x = a; a.inspect(); // Compile-time error (fortunately!) if you try to mutate/change the Fred at a: Fred y; a = y; // Fortunately(!) the compiler catches this error at compile-time a.mutate(); // Fortunately(!) the compiler catches this error at compile-time}

Il sovraccarico const per gli operatori pedice e funcall è illustrato qui,qui, qui, qui e qui.

Ovviamente puoi anche usare const -overloading per cose diverse dall’operatore pedice.

Come può aiutarmi a progettare classi migliori se distinguo lo stato logico dallo stato fisico?

Perché questo ti incoraggia a progettare le tue classi dall’esterno piuttosto che dall’interno, il che a sua volta rende le tue classi e gli oggetti più facili da capire e usare, più intuitivi, meno soggetti a errori e più veloci. (Ok, questa è una leggera semplificazione eccessiva. Per capire tutti i se e i ma, dovrai solo leggere il resto di thisanswer!)

Capiamo questo dall’interno-fuori — tu (dovresti) progettare le tue classi da theoutside-in, ma se sei nuovo a questo concetto, è più facile da capire da theinside-out.

All’interno, gli oggetti hanno uno stato fisico (o concreto o bit per bit). Questo è lo stato che è facile per i programmersto vedere e capire; è lo stato che sarebbe lì se la classe fosse solo uno stile C struct.

All’esterno, i tuoi oggetti hanno utenti della tua classe e questi utenti sono limitati a utilizzare solo publicmemberfunctions e friend s. Questi utenti esterni percepiscono anche l’oggetto come avente stato, ad esempio, se theobject è di classe Rectangle con metodi width(), height() e area(), gli utenti direbbero che quei tre sono tutti parte dello stato logico (o astratto o significativo) dell’oggetto. Per un utente esterno, l’oggetto Rectangle ha effettivamente un’area, anche se tale area viene calcolata al volo (ad esempio, se il metodo area() restituisce il prodotto della larghezza e dell’altezza dell’oggetto). In effetti, e questo è il punto importante, i tuoi utenti non sanno e non si preoccupano di come applichi uno di questi metodi; gli utenti percepiscono ancora, dal loro punto di vista, che l’oggetto ha logicamente uno stato di larghezza, altezza e area.

L’esempio area() mostra un caso in cui lo stato logico può contenere elementi che non sono direttamente realizzati nello stato fisico. È anche vero il contrario: le classi a volte nascondono intenzionalmente parte dello stato fisico(concreto, bit per bit) dei loro oggetti dagli utenti — intenzionalmente non forniscono alcuna funzione membro public ofriendche consentirebbe agli utenti di leggere o scrivere o addirittura conoscere questo stato nascosto. Ciò significa che ci sono bit nello stato fisico dell’oggetto che non hanno elementi corrispondenti nello stato logico dell’oggetto.

Come esempio di quest’ultimo caso, un oggetto-raccolta potrebbe memorizzare nella cache la sua ultima ricerca nella speranza di migliorare le prestazioni della sua prossima ricerca. Questa cache è certamente parte dello stato fisico dell’oggetto, ma c’è un dettaglio di implementazione interna che probabilmente non sarà esposto agli utenti — probabilmente non farà parte dello stato logico dell’oggetto. Raccontare ciò che è ciò che è facile se si pensa dall’esterno-in: se gli utenti dell’oggetto collection hanno noway per controllare lo stato della cache stessa, la cache è trasparente e non fa parte dello stato logico dell’oggetto.

La costanza delle mie funzioni membro pubbliche dovrebbe essere basata su ciò che il metodo fa allo stato logico o allo stato fisico dell’oggetto?

Logico.

Non c’è modo di rendere facile questa parte successiva. Farà male. La migliore raccomandazione è sedersi. E per favore, per la tua sicurezza, assicurati che non ci siano attrezzi affilati nelle vicinanze.

Torniamo all’esempio collection-object. Ricordare: c’è un metodo di ricerca che raggiunge l’ultima ricerca nella speranza di accelerare le ricerche future.

Diciamo ciò che è probabilmente ovvio: supponiamo che il metodo di ricerca non apporti modifiche allo stato logico di thecollection-object.

Quindi has è giunto il momento di farti del male. Sei pronto?

Ecco che arriva: se il metodo di ricerca non apporta alcuna modifica allo stato logico dell’oggetto di raccolta, ma modifica lo stato fisico dell’oggetto di raccolta (apporta una modifica molto reale alla cache molto reale), il metodo di ricerca dovrebbe essere const?

La risposta è un clamoroso Sì. (Ci sono eccezioni ad ogni regola, quindi ” Sì ” dovrebbe davvero avere un asterisco accanto ad esso,ma la stragrande maggioranza delle volte, la risposta è Sì.)

Si tratta di “logico const” su “fisico const.”Significa che la decisione se decorare amethod con const dovrebbe dipendere principalmente dal fatto che quel metodo lasci invariato lo stato logico, indipendentemente(sei seduto?) (potresti voler sederti) indipendentemente dal fatto che il metodo faccia cambiamenti molto reali allo stato fisico molto reale dell’oggetto.

Nel caso in cui ciò non sia affondato, o nel caso in cui non si abbia ancora dolore, prendiamolo in giro in due casi:

  • Se un metodo cambia qualsiasi parte dello stato logico dell’oggetto, logicamente è un mutatore; non dovrebbe essere const evenif (come effettivamente accade!) il metodo non modifica alcun bit fisico dello stato concreto dell’oggetto.
  • Al contrario, un metodo è logicamente un ispettore e dovrebbe essere const se non cambia mai alcuna parte dello stato logico dell’oggetto, anche se (come effettivamente accade!) il metodo cambia i bit fisici dello stato concreto dell’oggetto.

Se sei confuso, leggilo di nuovo.

Se non sei confuso ma sei arrabbiato, bene: potresti non piacerti ancora, ma almeno lo capisci. Fai un respiro profondoe ripeti dopo di me: “La constness di un metodo dovrebbe avere senso dall’esterno dell’oggetto.”

Se sei ancora arrabbiato, ripeti questo tre volte: “La costanza di un metodo deve avere senso per gli utenti dell’oggetto e quegli utenti possono vedere solo lo stato logico dell’oggetto.”

Se sei ancora arrabbiato, scusa, è quello che è. Succhialo e convivici. Sì, ci saranno delle eccezioni; ogni regola li ha. Ma di regola, nel complesso, questa nozione logica const è buona per te e buona per il tuo software.

Un’altra cosa. Questo diventerà inane, ma cerchiamo di essere precisi sul fatto che un metodo cambi lo stato logico dell’oggetto. Se sei al di fuori della classe — sei un utente normale, ogni esperimento che potresti eseguire (ogni metodo o sequenza di metodi che chiami) avrebbe gli stessi risultati (stessi valori di ritorno, stesse eccezioni o mancanza di eccezioni)indipendentemente dal fatto che tu abbia chiamato per la prima volta quel metodo di ricerca. Se la funzione di ricerca ha cambiato qualsiasi comportamento futuro di qualsiasi metodo futuro (non solo rendendolo più veloce ma ha cambiato il risultato, cambiato il valore restituito, cambiato l’eccezione), il metodo di ricerca ha cambiato lo stato logico dell’oggetto — è un mutuatore. Ma se il metodo di ricerca non ha cambiato altro che forse rendere alcune cose più veloci, allora è un ispettore.

Cosa devo fare se voglio che una funzione membro const apporti una modifica “invisibile” a un membro dati?

Usa mutable(o, come ultima risorsa, usa const_cast).

Una piccola percentuale di ispettori deve apportare modifiche allo stato fisico di un oggetto che non può essere osservato da externalusers — modifiche allo stato fisico ma non logico.

Ad esempio, l’oggetto-raccolta discusso in precedenza ha memorizzato nella cache l’ultima ricerca nella speranza di migliorare le prestazioni della successiva ricerca. Poiché la cache, in questo esempio, non può essere osservata direttamente da nessuna parte dell’interfaccia pubblica dell’oggetto raccolta (diversa dalla tempistica), la sua esistenza e lo stato non fanno parte dello stato logico dell’oggetto, quindi le modifiche ad esso sono invisibili agli utenti esterni. Il metodo di ricerca è un ispettore poiché non cambia mai lo stato logico dell’oggetto, indipendentemente dal fatto che, almeno per la presente implementazione, cambia lo stato fisico dell’oggetto.

Quando i metodi cambiano lo stato fisico ma non logico, il metodo dovrebbe generalmente essere contrassegnato come const poiché è davvero un metodo inspector. Ciò crea un problema: quando il compilatore vede il tuo metodo const cambiare lo stato fisico dell’oggetto this, si lamenterà-darà al tuo codice un messaggio di errore.

Il linguaggio del compilatore C++ utilizza la parola chiave mutable per aiutarti ad abbracciare questa nozione logica const. In questo caso, si contrassegna la cache con la parola chiave mutable, in questo modo il compilatore sa che è consentito cambiare all’interno di un metodoconst o tramite qualsiasi altro puntatore o riferimento const. Nel nostro gergo, la parola chiave mutable contrassegna quelle porzioni dello stato fisico dell’oggetto che non fanno parte dello stato logico.

La parola chiave mutableva appena prima della dichiarazione del membro dei dati, ovvero nello stesso punto in cui è possibile inserireconst. L’altro approccio, non preferito, è quello di gettare via il const‘ness del this puntatore, probabilmente tramite ilconst_cast parola chiave:

Set* self = const_cast<Set*>(this); // See the NOTE below before doing this!

Dopo questa linea, self avrà lo stesso bit come this, ovvero self == this, ma self è un Set* invece diconst Set* (tecnicamente this è un const Set* const, ma più a destra const è irrilevante per la discussione).Ciò significa che puoi usare selfper modificare l’oggetto a cui punta this.

NOTA: c’è un errore estremamente improbabile che può verificarsi con const_cast. Succede solo quando tre cose molto rarele cose sono combinate allo stesso tempo: un membro dei dati che dovrebbe essere mutable (come discusso sopra), un compiler che non supporta la parola chiave mutable e/o un programmatore che non la utilizza e un oggetto che è stato originariamente definito come const (al contrario di un normale oggetto nonconsta cui è puntato un puntatore-a-const).Sebbene questa combinazione sia così rara che potrebbe non accadere mai a te, se mai accadesse, il codice potrebbe non funzionare (lo standard dice che il comportamento non è definito).

Se vuoi usare const_cast, usa invece mutable. In altre parole, se hai bisogno di cambiare un membro di anobject e quell’oggetto è puntato da un puntatore a-const, la cosa più sicura e semplice da fare è aggiungere mutable alla dichiarazione del membro. Puoi usare const_cast se sei sicuro che l’oggetto reale non sia const (ad esempio, se sei sicuro che l’oggetto sia dichiarato in questo modo: Set s;), ma se l’oggetto stesso potrebbe essere const (ad esempio, ifit potrebbe essere dichiarato come: const Set s;), usa mutablepiuttosto che const_cast.

Per favore non scrivere dicendo che la versione X del compilatore Y sulla macchina Z ti consente di modificare un membro nonmutable di un oggettoconst. Non mi interessa-è illegale in base alla lingua e il tuo codice probabilmente fallirà su un differentcompiler o anche su una versione diversa (un aggiornamento) dello stesso compilatore. Di ‘ solo di no. Usa invece mutable. Scrivi codeche è garantito per funzionare, non codice che non sembra rompersi.

Const_cast significa opportunità di ottimizzazione perse?

In teoria, sì; in pratica, no.

Anche se la lingua fuorilegge const_cast, l’unico modo per evitare di svuotare la cache del registro attraverso una chiamata memberfunction const sarebbe risolvere il problema dell’aliasing (cioè, per dimostrare che non ci sono puntatori nonconst che puntino all’oggetto. Questo può accadere solo in rari casi (quando l’oggetto è costruito nell’ambito del const memberfunction invocazione, e quando tutti i non-const membro chiamate di funzione tra l’oggetto la costruzione e laconst funzione membro invocazione sono staticamente legato, e quando ogni una di queste invocazioni è anche inlined, andwhen costruttore stesso è inlined, e quando un membro funzioni costruttore chiamate inline).

Perché il compilatore mi consente di cambiare un int dopo averlo indicato con un const int*?

Perché “const int* p” significa “p promette di non cambiare*p,” non ” *p promette di non cambiare.”

Causando un const int* per puntare a un intnon const-ify int. int non può essere modificato tramiteconst int*, ma se qualcun altro ha un int* (nota: no const) che punta a (“alias”) lo stesso int, alloraint*può essere usato per cambiare int. Biru:

void f(const int* p1, int* p2){ int i = *p1; // Get the (original) value of *p1 *p2 = 7; // If p1 == p2, this will also change *p1 int j = *p1; // Get the (possibly new) value of *p1 if (i != j) { std::cout << "*p1 changed, but it didn't change via pointer p1!\n"; assert(p1 == p2); // This is the only way *p1 could be different }}int main(){ int x = 5; f(&x, &x); // This is perfectly legal (and even moral!) // ...}

Si noti che main() e f(const int*,int*) potrebbero trovarsi in diverse unità di compilazione compilate in giorni diversidella settimana. In tal caso non c’è modo che il compilatore possa rilevare l’aliasing in fase di compilazione. Pertanto thereis alcun modo potremmo fare una regola linguistica che vieta questo genere di cose. In realtà, non vorremmo nemmeno fare un tale arule, dal momento che in generale è considerata una caratteristica che puoi avere molti puntatori che puntano alla stessa cosa. Il fatto che uno di quei puntatori promette di non cambiare la “cosa” sottostante è solo una promessa fatta dal puntatore; non è una promessa fatta dalla”cosa”.

“const Fred * p” significa che *p non può cambiare?

No! (Questo è correlato alle FAQ sull’aliasing dei puntatori int.)

const Fred* p” significa che Fred non può essere modificato tramite il puntatore p, ma potrebbero esserci altri modi per arrivare all’oggetto senza passare attraverso un puntatore const (come un puntatore nonconstcon alias come Fred*). Ad esempio, se hai due puntatori “const Fred* p” e “Fred* q” che puntano allo stesso oggetto Fred (aliasing), pointer q può essere utilizzato per modificare l’oggetto Fred ma pointer p non può.

class Fred {public: void inspect() const; // A const member function void mutate(); // A non-const member function};int main(){ Fred f; const Fred* p = &f; Fred* q = &f; p->inspect(); // Okay: No change to *p p->mutate(); // Error: Can't change *p via p q->inspect(); // Okay: q is allowed to inspect the object q->mutate(); // Okay: q is allowed to mutate the object f.inspect(); // Okay: f is allowed to inspect the object f.mutate(); // Okay: f is allowed to mutate the object // ...}

Perché ricevo un errore nel convertire un Foo * * → const Foo**?

Perché la conversione Foo**const Foo** non sarebbe valida e pericolosa.

C++ consente la conversione (sicura) Foo*Foo const*, ma fornisce un errore se si tenta di convertire implicitamente Foo**const Foo**.

La motivazione per cui quell’errore è una buona cosa è riportata di seguito. Ma prima, ecco la soluzione più comune: simplychange const Foo** a const Foo* const*:

class Foo { /* ... */ };void f(const Foo** p);void g(const Foo* const* p);int main(){ Foo** p = /*...*/; // ... f(p); // ERROR: it's illegal and immoral to convert Foo** to const Foo** g(p); // Okay: it's legal and moral to convert Foo** to const Foo* const* // ...}

Il motivo per cui la conversione da Foo**const Foo** è pericolosa è che ti consente di modificare silenziosamente e accidentalmente un oggetto const Foo senza cast:

class Foo {public: void modify(); // make some modification to the this object};int main(){ const Foo x; Foo* p; const Foo** q = &p; // q now points to p; this is (fortunately!) an error *q = &x; // p now points to x p->modify(); // Ouch: modifies a const Foo!! // ...}

Se la linea q = &p fosse legale, q punterebbe a p. La riga successiva, *q = &x, cambia p stesso (poiché *qè p) per puntare a x. Sarebbe una brutta cosa, dal momento che avremmo perso il qualificatore const: p è un Foo* max è un const Foo. La linea p->modify() sfrutta la capacità di pdi modificare il suo referente,che è il vero problema, dal momento che abbiamo finito per modificare un const Foo.

Per analogia, se si nasconde un criminale sotto un travestimento legale, può quindi sfruttare la fiducia data a quel travestimento.Non va bene.

Per fortuna C++ ti impedisce di farlo: la riga q = &p viene contrassegnata dal compilatore C++ come errore di compilazione. Promemoria: si prega di non lanciare il puntatore per aggirare il messaggio di errore in fase di compilazione. Basta dire di no!

(Nota: esiste una somiglianza concettuale tra questo e il divieto di convertire Derived**inBase**.)

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.