Dovrei restituire oggetti const?

In Effective C++ Item 03, Usa const quando ansible.

 class Bigint { int _data[MAXLEN]; //... public: int& operator[](const int index) { return _data[index]; } const int operator[](const int index) const { return _data[index]; } //... }; 

const int operator[] fa la differenza da int& operator[] .

Ma per quanto riguarda:

 int foo() { } 

e

 const int foo() { } 

Sembra che siano uguali.

La mia domanda è, perché usiamo const int operator[](const int index) const invece di int operator[](const int index) const ?

I qualificatori di cv di livello superiore sui tipi di ritorno di tipo non di class vengono ignorati. Il che significa che anche se scrivi:

 int const foo(); 

Il tipo di ritorno è int . Se il tipo di ritorno è un riferimento, ovviamente, const non è più di primo livello e la distinzione tra:

 int& operator[]( int index ); 

e

 int const& operator[]( int index ) const; 

è significativo (Nota anche che nelle dichiarazioni di funzioni, come sopra, anche i qualificatori di cv di primo livello vengono ignorati).

La distinzione è rilevante anche per i valori di ritorno del tipo di class: se si restituisce T const , il chiamante non può richiamare le funzioni non-const sul valore restituito, ad esempio:

 class Test { public: void f(); void g() const; }; Test ff(); Test const gg(); ff().f(); // legal ff().g(); // legal gg().f(); // **illegal** gg().g(); // legal 

È necessario distinguere chiaramente tra l’uso const applicato per restituire valori, parametri e la funzione stessa.

Valori di ritorno

  • Se la funzione restituisce per valore , cost non ha importanza, poiché viene restituita la copia dell’object. Sarà comunque importante in C ++ 11 con la semantica del movimento coinvolta.
  • Inoltre non ha mai importanza per i tipi di base, poiché vengono sempre copiati.

Considera const std::string SomeMethod() const . Non consentirà l’utilizzo della funzione (std::string&&) , in quanto prevede un valore non const. In altre parole, la stringa restituita verrà sempre copiata.

  • Se la funzione restituisce per riferimento , const protegge l’object restituito dalla modifica.

parametri

  • Se si passa un parametro per valore , il const impedisce la modifica del valore dato per funzione in funzione . I dati originali del parametro non possono essere modificati comunque, poiché hai solo una copia.
  • Si noti che poiché la copia viene sempre creata, const ha solo il significato per il corpo della funzione , quindi viene controllato solo nella definizione della funzione, non nella dichiarazione (interfaccia).
  • Se si passa un parametro per riferimento , si applica la stessa regola dei valori di ritorno

Funzione stessa

  • Se la funzione ha const alla fine, può solo eseguire altre funzioni const e non può modificare o consentire la modifica dei dati della class. Pertanto, se restituisce per riferimento, il riferimento restituito deve essere const. Solo le funzioni const possono essere richiamate su un object o su un riferimento all’object che è const stesso. Anche i campi mutabili possono essere modificati.
  • Il comportamento creato dal compilatore cambia this riferimento in T const* . La funzione può sempre const_cast this , ma ovviamente questo non dovrebbe essere fatto ed è considerato pericoloso.
  • Ovviamente è ragionevole usare questo identificatore solo sui metodi di class; le funzioni globali con const alla fine aumentano l’errore di compilazione.

Conclusione

Se il tuo metodo non modifica e non modificherà mai le variabili di class, contrassegnalo come const e assicurati di soddisfare tutte le criticità necessarie. Permetterà di scrivere un codice più pulito, mantenendolo costante . Tuttavia, mettere const dappertutto senza pensarci certo non è la strada da percorrere.

C’è poco valore nell’aggiungere le qualifiche const ai valori non di riferimento / non puntatore, e non ha senso aggiungerlo ai built-in.

Nel caso di tipi definiti dall’utente , una qualifica const impedisce ai chiamanti di richiamare una funzione membro non const sull’object restituito. Ad esempio, dato

 const std::string foo(); std::string bar(); 

poi

 foo().resize(42); 

sarebbe proibito, mentre

 bar().resize(4711); 

sarebbe permesso.

Per i built-in come int , questo non ha alcun senso, perché tali valori non possono essere comunque modificati.

(Ricordo che Effective C ++ discute di rendere il tipo di ritorno di operator=() un riferimento const , però, e questo è qualcosa da considerare.)


Modificare:

Sembra che Scott abbia davvero dato quel consiglio . Se è così, a causa delle ragioni sopra riportate, lo trovo discutibile anche per C ++ 98 e C ++ 03 . Per C ++ 11, lo considero chiaramente sbagliato , come Scott stesso sembra aver scoperto. Nell’errata per Effective C ++, 3a ed. , scrive (o cita altri che si sono lamentati):

Il testo implica che tutti i ritorni in base al valore devono essere const, ma i casi in cui i ritorni in base al valore non const non sono difficili da trovare, ad esempio, restituiscono i tipi di std :: vector dove i chiamanti utilizzeranno lo swap con un vettore vuoto per “afferrare” il contenuto dei valori restituiti senza copiarli.

E più tardi:

Se si dichiarano i valori di ritorno della funzione di valore-by, const impedirà che vengano associati a valori di riferimento in C ++ 0x. Poiché i riferimenti di rvalue sono progettati per aiutare a migliorare l’efficienza del codice C ++, è importante tenere in considerazione l’interazione dei valori di ritorno const e l’inizializzazione dei riferimenti rvalue quando si specificano le firme delle funzioni.

Potresti perdere il consiglio di Meyers. La differenza essenziale è nel modificatore di const per il metodo.

Questo è un metodo non const (avviso no const alla fine) che significa che è consentito modificare lo stato della class.

 int& operator[](const int index) 

Questo è un metodo const ( const avviso alla fine)

 const int operator[](const int index) const 

Per quanto riguarda i tipi di parametro e il valore di ritorno, esiste una differenza minore tra int e const int , ma non è pertinente al punto del consiglio. Cosa dovresti fare attenzione al fatto che il sovraccarico non const restituisce int& che significa che puoi assegnarlo ad esso, ad esempio num[i]=0 , e il sovraccarico const restituisce un valore non modificabile (non importa se il tipo restituito è int o const int ).

Secondo la mia opinione personale, se un object è passato per valore , il modificatore di const è superfluo. Questa syntax è più breve e raggiunge lo stesso risultato

 int& operator[](int index); int operator[](int index) const; 

Il motivo principale per cui si restituiscono valori come const è che non si può dire qualcosa come foo() = 5; . Questo non è in realtà un problema con i tipi primitivi, dal momento che non è ansible assegnare a rvalues ​​di tipi primitivi, ma è un problema con tipi definiti dall’utente (come (a + b) = c; con un operator+ sovraccarico operator+ ).

Ho sempre trovato la giustificazione per questo piuttosto inconsistente. Non puoi fermare qualcuno che è intenzionato a scrivere codice scomodo e questo particolare tipo di coercizione non ha alcun beneficio reale a mio avviso.

Con C ++ 11, c’è in realtà un bel po ‘di danno che questo idioma sta facendo: restituire valori come const previene le ottimizzazioni del movimento e dovrebbe quindi essere evitato quando ansible. Fondamentalmente, ora considererei questo un anti-pattern.

Ecco un articolo correlato tangenzialmente a C ++ 11.

Quando il libro è stato scritto, il consiglio era di scarsa utilità, ma serviva a impedire che l’utente scrivesse, ad esempio, foo() = 42; e mi aspetto che cambi qualcosa di persistente.

Nel caso operator[] , questo può essere leggermente confuso se non si fornisce anche un overload non const che restituisce un riferimento non const , anche se è ansible evitare tale confusione restituendo invece un riferimento const o un object proxy di un valore.

In questi giorni, è un cattivo consiglio, dal momento che impedisce di bind il risultato a un riferimento di valore (non costante).

(Come sottolineato nei commenti, la domanda è discutibile quando si restituisce un tipo primitivo come int , poiché il linguaggio impedisce l’assegnazione a un rvalue di tale tipo; sto parlando del caso più generale che include il ritorno definito dall’utente i tipi.)

Per i tipi primitivi (come int ), la costanza del risultato non ha importanza. Per le classi, potrebbe cambiare il comportamento. Ad esempio, potresti non essere in grado di chiamare un metodo non const sul risultato della tua funzione:

 class Bigint { const C foo() const { ... } ... } Bigint b; b.foo().bar(); 

Quanto sopra è vietato se bar() è una funzione membro const di C In generale, scegli quello che ha senso.

Guarda su questo:

 const int operator[](const int index) const 

il const alla fine della dichiarazione. Descrive che questo metodo può essere chiamato sulle costanti.

D’altra parte quando scrivi solo

 int& operator[](const int index) 

può essere chiamato solo su istanze non const e fornisce anche:

 big_int[0] = 10; 

syntax.

Uno dei sovraccarichi restituisce un riferimento a un elemento nell’array, che è quindi ansible modificare.

 int& operator[](const int index) { return _data[index]; } 

L’altro sovraccarico restituisce un valore da utilizzare.

 const int operator[](const int index) const { return _data[index]; } 

Dal momento che chiami ognuno di questi sovraccarichi nello stesso modo, e uno non cambierà mai il valore quando viene usato.

 int foo = myBigInt[1]; // Doesn't change values inside the object. myBigInt[1] = 2; // Assigns a new value at index ` 

Nell’esempio dell’operatore di indicizzazione dell’array ( operator[] ) fa la differenza.

Con

 int& operator[](const int index) { /* ... */ } 

puoi usare l’indicizzazione per cambiare direttamente la voce nell’array, ad esempio usandola in questo modo:

 mybigint[3] = 5; 

Il secondo const int operator[](const int) viene utilizzato per recuperare solo il valore.

Tuttavia, come valore di ritorno dalle funzioni, per tipi semplici come eg int , non ha importanza. Quando importa è se si restituiscono tipi più complessi, ad esempio un std::vector e non si desidera che il chiamante della funzione modifichi il vettore.

Il tipo di ritorno const non è così importante qui. Poiché i temporari int non sono modificabili, non vi è alcuna differenza osservabile nell’uso di int vs const int . Vedresti una differenza se usassi un object più complesso che potrebbe essere modificato.

Ci sono alcune buone risposte che ruotano attorno alla tecnicità delle due versioni. Per un valore primitivo, non fa differenza.

Tuttavia , ho sempre considerato const di essere lì per il programmatore piuttosto che per il compilatore. Quando scrivi const , stai esplicitamente dicendo “questo non dovrebbe cambiare”. Ammettiamolo, const solito può essere comunque circoscritto, giusto?

Quando restituisci un const , stai dicendo al programmatore che usa quella funzione che il valore non dovrebbe cambiare. E se lo sta cambiando, probabilmente sta facendo qualcosa di sbagliato, perché lui / lei non dovrebbe.

EDIT: Penso anche che “usare const quando ansible” è un cattivo consiglio. Dovresti usarlo dove ha senso.