La pratica di restituire una variabile di riferimento C ++ è male?


341

Questo è un po 'soggettivo, penso; Non sono sicuro che l'opinione sarà unanime (ho visto molti frammenti di codice in cui vengono restituiti riferimenti).

Secondo un commento a questa domanda che ho appena fatto, riguardo all'inizializzazione dei riferimenti , restituire un riferimento può essere malvagio perché, [come ho capito], rende più facile perdere la sua eliminazione, il che può portare a perdite di memoria.

Questo mi preoccupa, dato che ho seguito degli esempi (a meno che non stia immaginando cose) e l'ho fatto in pochi posti ... Ho capito male? È male? Se è così, quanto male?

Sento che a causa del mio mix di puntatori e riferimenti, combinato con il fatto che sono nuovo in C ++ e la totale confusione su cosa usare quando, le mie applicazioni devono essere un inferno di perdita di memoria ...

Inoltre, capisco che l'utilizzo di puntatori intelligenti / condivisi è generalmente accettato come il modo migliore per evitare perdite di memoria.


Non è male se stai scrivendo funzioni / metodi getter-like.
John Z. Li,

Risposte:


411

In generale, la restituzione di un riferimento è perfettamente normale e avviene sempre.

Se intendi:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

Questo è ogni sorta di male. Lo stack allocato iscomparirà e non ti riferirai a nulla. Anche questo è male:

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

Perché ora il cliente deve eventualmente fare lo strano:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

Nota che i riferimenti rvalue sono ancora solo riferimenti, quindi tutte le applicazioni malvagie rimangono le stesse.

Se si desidera allocare qualcosa che non rientra nell'ambito della funzione, utilizzare un puntatore intelligente (o in generale un contenitore):

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

E ora il client memorizza un puntatore intelligente:

std::unique_ptr<int> x = getInt();

I riferimenti vanno anche bene per l'accesso a cose in cui sai che la vita viene mantenuta aperta a un livello superiore, ad esempio:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Qui sappiamo che va bene restituire un riferimento i_perché qualunque cosa ci chiami gestisce la durata dell'istanza della classe, quindi i_vivrà almeno così a lungo.

E, naturalmente, non c'è niente di sbagliato nel solo:

int getInt() {
   return 0;
}

Se la durata deve essere lasciata al chiamante e stai solo calcolando il valore.

Riepilogo: è possibile restituire un riferimento se la durata dell'oggetto non termina dopo la chiamata.


21
Questi sono tutti cattivi esempi. Il miglior esempio di utilizzo corretto è quando si restituisce un riferimento a un oggetto che è stato passato. Operatore operatore <<
Arelius

171
Per il bene dei posteri e per tutti i programmatori più recenti che cambiano argomento, i puntatori non sono male . Né i puntatori alla memoria dinamica sono cattivi. Entrambi hanno i loro posti legittimi in C ++. I puntatori intelligenti dovrebbero essere sicuramente il punto di partenza predefinito quando si tratta di gestione dinamica della memoria, ma il puntatore intelligente predefinito dovrebbe essere unique_ptr, non shared_ptr.
Jamin Gray

12
Modifica approvatori: non approvare le modifiche se non puoi garantirne la correttezza. Ho eseguito il rollback della modifica errata.
GManNickG

7
Per il bene dei posteri e per tutti i programmatori più recenti che cambiano argomento, non scriverereturn new int .
Razze di leggerezza in orbita

3
Per il bene dei posteri e per tutti i programmatori più recenti che cambiano argomento, basta restituire la T dalla funzione. RVO si occuperà di tutto.
Scarpa

64

No. No, no, mille volte no.

Ciò che è male è fare riferimento a un oggetto allocato dinamicamente e perdere il puntatore originale. Quando sei newun oggetto, ti impegni ad avere una garanzia delete.

Ma dai un'occhiata, ad esempio, operator<<che deve restituire un riferimento, oppure

cout << "foo" << "bar" << "bletch" << endl ;

non funzionerà.


23
Ho effettuato il downvoting perché questo non risponde alla domanda (in cui OP ha chiarito che conosce la necessità di eliminazione) né affronta il legittimo timore che restituire un riferimento a un oggetto freestore possa creare confusione. Sospiro.

4
La pratica di restituire un oggetto di riferimento non è malvagia. Ergo no. La paura che esprime è una paura corretta, come faccio notare nel secondo graf.
Charlie Martin,

In realtà non l'hai fatto. Ma questo non vale il mio tempo.

2
Iraimbilanja @ A proposito dei "No" non mi interessa. ma questo post ha sottolineato una informazione importante che mancava a GMan.
Kobor42,

48

È necessario restituire un riferimento a un oggetto esistente che non scompare immediatamente e in cui non si intende alcun trasferimento di proprietà.

Non restituire mai un riferimento a una variabile locale o ad una simile, perché non sarà lì per essere referenziato.

Puoi restituire un riferimento a qualcosa di indipendente dalla funzione, che non ti aspetti che la funzione chiamante si assuma la responsabilità dell'eliminazione. Questo è il caso della operator[]funzione tipica .

Se stai creando qualcosa, dovresti restituire un valore o un puntatore (normale o intelligente). È possibile restituire un valore liberamente, poiché sta entrando in una variabile o espressione nella funzione chiamante. Non restituire mai un puntatore a una variabile locale, poiché andrà via.


1
Risposta eccellente ma per "Puoi restituire un temporaneo come riferimento const". Il seguente codice verrà compilato ma probabilmente si arresterà in modo anomalo perché il temporaneo viene distrutto alla fine dell'istruzione return: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker

@j_random_hacker: C ++ ha alcune strane regole per i riferimenti ai provvisori, dove la durata temporanea potrebbe essere estesa. Mi dispiace non capisco abbastanza bene per sapere se copre il tuo caso.
Mark Ransom,

3
@Mark: Sì, ha delle regole strane. La durata di un temporaneo può essere estesa solo inizializzando un riferimento const (che non è un membro di classe) con esso; quindi vive fino a quando l'arbitro non esce dal campo di applicazione. Purtroppo, la restituzione di un riferimento const non è coperta. Restituire una temperatura in base al valore è comunque sicuro.
j_random_hacker,

Vedi lo standard C ++, 12.2, paragrafo 5. Vedi anche il randagio Guru della settimana di Herb Sutter su Herbsutter.wordpress.com/2008/01/01/… .
David Thornley,

4
@David: quando il tipo restituito della funzione è "T const &", ciò che effettivamente accade è che l'istruzione return converte implicitamente la temp, che è di tipo T, per digitare "T const &" come da 6.6.3.2 (una conversione legale ma una che non estende la durata), quindi il codice chiamante inizializza il riferimento di tipo "T const &" con il risultato della funzione, anche di tipo "T const &" - ​​ancora una volta, un processo legale ma che non prolunga la vita. Risultato finale: nessuna estensione della vita e molta confusione. :(
j_random_hacker,

26

Trovo le risposte non soddisfacenti, quindi aggiungerò i miei due centesimi.

Analizziamo i seguenti casi:

Uso errato

int& getInt()
{
    int x = 4;
    return x;
}

Questo è ovviamente un errore

int& x = getInt(); // will refer to garbage

Utilizzo con variabili statiche

int& getInt()
{
   static int x = 4;
   return x;
}

Questo è giusto, perché le variabili statiche sono presenti per tutta la durata di un programma.

int& x = getInt(); // valid reference, x = 4

Questo è anche abbastanza comune quando si implementa il modello Singleton

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

Uso:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

operatori

I contenitori di librerie standard dipendono fortemente dall'utilizzo di operatori che restituiscono riferimenti, ad esempio

T & operator*();

può essere utilizzato di seguito

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

Accesso rapido ai dati interni

Ci sono momenti in cui e possono essere utilizzati per un rapido accesso ai dati interni

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

con l'utilizzo:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

TUTTAVIA, ciò può causare insidie ​​come questa:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!

Restituire il riferimento a una variabile statica può portare a comportamenti indesiderati, ad esempio prendere in considerazione un operatore moltiplicatore che restituisce un riferimento a un membro statico, per cui si verificherà sempre quanto segue true:If((a*b) == (c*d))
SebNag

Container::data()L 'implementazione dovrebbe essere lettareturn m_data;
Xeaz il

Questo è stato molto utile, grazie! @Xeaz non causerebbe problemi con la chiamata append?
Andrew,

@SebTu Perché dovresti farlo?
thorhunter,

@Andrew No, era uno shenanigan di sintassi. Se, ad esempio, si restituisce un tipo di puntatore, utilizzare l'indirizzo di riferimento per creare e restituire un puntatore.
thorhunter,

14

Non è male. Come molte cose in C ++, è buono se usato correttamente, ma ci sono molte insidie ​​di cui dovresti essere consapevole quando lo usi (come restituire un riferimento a una variabile locale).

Ci sono buone cose che possono essere raggiunte con esso (come map [name] = "hello world")


1
Sono solo curioso, di cosa si tratta map[name] = "hello world"?
wrongusername

5
@wrongusername La sintassi è intuitiva. Hai mai provato ad aumentare il conteggio di un valore memorizzato in a HashMap<String,Integer>in Java? : P
Mehrdad Afshari,

Ahah, non ancora, ma guardando gli esempi di HashMap, sembra piuttosto nodoso: D
wrongusername

Problema riscontrato con questo: la funzione restituisce il riferimento a un oggetto in un contenitore, ma il codice della funzione chiamante lo ha assegnato a una variabile locale. Quindi modificato alcune proprietà dell'oggetto. Problema: l'oggetto originale nel contenitore è rimasto intatto. Il programmatore trascura così facilmente il valore & nel valore restituito, e quindi ottieni comportamenti davvero inaspettati ...
flohack

10

"restituire un riferimento è male perché, semplicemente [come ho capito], rende più facile perdere la sua eliminazione"

Non vero. La restituzione di un riferimento non implica la semantica della proprietà. Cioè, solo perché lo fai:

Value& v = thing->getTheValue();

... non significa che ora possiedi la memoria a cui fa riferimento v;

Tuttavia, questo è un codice orribile:

int& getTheValue()
{
   return *new int;
}

Se stai facendo qualcosa del genere perché "non hai bisogno di un puntatore su quell'istanza" allora: 1) dereferenzia il puntatore solo se hai bisogno di un riferimento, e 2) alla fine avrai bisogno del puntatore, perché devi abbinare un nuovo con un'eliminazione e per chiamare l'eliminazione è necessario un puntatore.


7

Esistono due casi:

  • const const - buona idea, a volte, soprattutto per oggetti pesanti o classi proxy, ottimizzazione del compilatore

  • riferimento non const - l'idea di idea, a volte, rompe gli incapsulamenti

Entrambi condividono lo stesso problema: possono potenzialmente puntare a oggetti distrutti ...

Consiglio di utilizzare i puntatori intelligenti per molte situazioni in cui è necessario restituire un riferimento / puntatore.

Inoltre, tenere presente quanto segue:

Esiste una regola formale - lo standard C ++ (sezione 13.3.3.1.4 se sei interessato) afferma che un temporaneo può essere associato solo a un riferimento const - se si tenta di utilizzare un riferimento non const il compilatore deve contrassegnarlo come un errore.


1
ref non const non rompe necessariamente l'incapsulamento. considera vector :: operator []

questo è un caso molto speciale ... è per questo che ho detto a volte, anche se dovrei davvero rivendicare LA MAGGIOR PARTE DEL TEMPO :)

Quindi, stai dicendo che l'implementazione dell'operatore di sottoscrizione normale è un male necessario? Non sono né d'accordo né d'accordo con questo; come non sono nessuno il più saggio.
Nick Bolton,

Non lo dico, ma può essere male se usato in modo improprio :))) vector :: at dovrebbe essere usato ogni volta che è possibile ....

eh? vector :: at restituisce anche un ref nonconst.

4

Non solo non è cattivo, a volte è essenziale. Ad esempio, sarebbe impossibile implementare l'operatore [] di std :: vector senza usare un valore di ritorno di riferimento.


Ah, sì certo; Penso che sia per questo che ho iniziato a usarlo; come quando ho implementato per la prima volta l'operatore di sottoscrizione [] mi sono reso conto dell'uso dei riferimenti. Sono portato a credere che questo sia di fatto.
Nick Bolton,

Stranamente, puoi implementare operator[]un contenitore senza usare un riferimento ... e lo std::vector<bool>fa. (E crea un vero casino nel processo)
Ben Voigt

@BenVoigt mmm, perché un casino? La restituzione di un proxy è anche uno scenario valido per contenitori con archiviazione complessa che non si associa direttamente ai tipi esterni (come ::std::vector<bool>hai già detto).
Sergey.quixoticaxis.Ivanov,

1
@ Sergey.quixoticaxis.Ivanov: il casino è che l'uso std::vector<T>del codice modello è rotto, se Tpossibile bool, perché std::vector<bool>ha un comportamento molto diverso dalle altre istanze. È utile, ma avrebbe dovuto avere un nome proprio e non una specializzazione di std::vector.
Ben Voigt,

@BenVoight Sono d'accordo sul punto sulla strana decisione di rendere una specializzazione "davvero speciale", ma ho sentito che il tuo commento originale implica che restituire un proxy è strano in generale.
Sergey.quixoticaxis.Ivanov,

2

Aggiunta alla risposta accettata:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Direi che questo esempio non va bene e, se possibile, dovrebbe essere evitato. Perché? È molto facile finire con un riferimento penzolante .

Per illustrare il punto con un esempio:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

entrare nella zona di pericolo:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!

1

il riferimento di ritorno viene generalmente utilizzato nel sovraccarico dell'operatore in C ++ per oggetti di grandi dimensioni, poiché la restituzione di un valore richiede un'operazione di copia (nel sovraccarico del peratore, di solito non si utilizza il puntatore come valore di ritorno)

Ma il riferimento di ritorno può causare problemi di allocazione della memoria. Poiché un riferimento al risultato verrà passato dalla funzione come riferimento al valore restituito, il valore restituito non può essere una variabile automatica.

se si desidera utilizzare il riferimento di ritorno, è possibile utilizzare un buffer di oggetto statico. per esempio

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

in questo modo, è possibile utilizzare il riferimento di ritorno in modo sicuro.

Ma puoi sempre usare il puntatore invece del riferimento per restituire valore in functiong.


0

penso che usare il riferimento come valore di ritorno della funzione sia molto più semplice che usare il puntatore come valore di ritorno della funzione. In secondo luogo, sarebbe sempre sicuro utilizzare una variabile statica a cui si riferisce il valore restituito.


0

La cosa migliore è creare un oggetto e passarlo come parametro di riferimento / puntatore a una funzione che alloca questa variabile.

Allocare l'oggetto in funzione e restituirlo come riferimento o puntatore (il puntatore è più sicuro) è una cattiva idea a causa della liberazione di memoria alla fine del blocco funzione.


-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

La funzione getPtr può accedere alla memoria dinamica dopo l'eliminazione o anche un oggetto null. Che può causare eccezioni di accesso errato. Invece getter e setter dovrebbero essere implementati e le dimensioni verificate prima di tornare.


-2

La funzione come lvalue (ovvero il ritorno di riferimenti non costanti) deve essere rimossa dal C ++. È terribilmente non intuitivo. Scott Meyers voleva un min () con questo comportamento.

min(a,b) = 0;  // What???

che non è davvero un miglioramento

setmin (a, b, 0);

Quest'ultimo ha persino più senso.

Mi rendo conto che la funzione come lvalue è importante per i flussi in stile C ++, ma vale la pena sottolineare che i flussi in stile C ++ sono terribili. Non sono l'unico a pensarlo ... mentre ricordo che Alexandrescu aveva un ampio articolo su come fare di meglio, e credo che boost abbia anche provato a creare un metodo I / O sicuro di tipo migliore.


2
Certo, è pericoloso e dovrebbe esserci un migliore controllo degli errori del compilatore, ma senza di esso alcuni costrutti utili non potrebbero essere fatti, ad esempio operator [] () in std :: map.
j_random_hacker,

2
Restituire riferimenti non costanti è in realtà incredibilmente utile. vector::operator[]per esempio. Preferiresti scrivere v.setAt(i, x)o v[i] = x? Quest'ultimo è di gran lunga superiore.
Miles Rout

1
@MilesRout Vorrei andare per v.setAt(i, x)qualsiasi momento. È di gran lunga superiore.
scravy

-2

Mi sono imbattuto in un vero problema in cui era davvero malvagio. In sostanza uno sviluppatore ha restituito un riferimento a un oggetto in un vettore. È stato male !!!

I dettagli completi di cui ho scritto a gennaio: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
Se è necessario modificare il valore originale nel codice chiamante, è necessario restituire un riferimento. E questo in realtà non è più né meno pericoloso che restituire un iteratore a un vettore: entrambi vengono invalidati se gli elementi vengono aggiunti o rimossi dal vettore.
j_random_hacker,

Quel particolare problema è stato causato dal mantenimento di un riferimento a un elemento vettoriale e quindi dalla modifica di quel vettore in modo da invalidare il riferimento: Pagina 153, sezione 6.2 di "Libreria standard C ++: un tutorial e riferimento" - Josuttis, recita: "Inserimento o la rimozione di elementi invalida riferimenti, puntatori e iteratori che fanno riferimento ai seguenti elementi. Se un inserimento provoca riallocazione, invalida tutti i riferimenti, iteratori e puntatori "
Trento

-15

Informazioni sul codice orribile:

int& getTheValue()
{
   return *new int;
}

Quindi, in effetti, il puntatore della memoria ha perso dopo il ritorno. Ma se usi shared_ptr in questo modo:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

La memoria non viene persa dopo la restituzione e verrà liberata dopo l'assegnazione.


12
Si perde perché il puntatore condiviso non rientra nell'ambito e libera l'intero.

il puntatore non viene perso, l'indirizzo del riferimento è il puntatore.
dgsomerton,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.