Come "restituire un oggetto" in C ++?


167

So che il titolo sembra familiare dato che ci sono molte domande simili, ma sto chiedendo un aspetto diverso del problema (conosco la differenza tra avere le cose in pila e metterle in pila).

In Java posso sempre restituire riferimenti ad oggetti "locali"

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

In C ++, per fare qualcosa di simile ho 2 opzioni

(1) Posso usare i riferimenti ogni volta che devo "restituire" un oggetto

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

Quindi usalo in questo modo

Thing thing;
calculateThing(thing);

(2) Oppure posso restituire un puntatore a un oggetto allocato dinamicamente

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

Quindi usalo in questo modo

Thing* thing = calculateThing();
delete thing;

Usando il primo approccio non dovrò liberare memoria manualmente, ma per me rende difficile leggere il codice. Il problema con il secondo approccio è che dovrò ricordarmene delete thing;, il che non sembra abbastanza carino. Non voglio restituire un valore copiato perché è inefficiente (credo), quindi ecco le domande

  • Esiste una terza soluzione (che non richiede la copia del valore)?
  • C'è qualche problema se mi attengo alla prima soluzione?
  • Quando e perché dovrei usare la seconda soluzione?

32
+1 per porre bene la domanda.
Kangkan,

1
Per essere molto pedanti, è un po 'impreciso dire che "le funzioni restituiscono qualcosa". Più correttamente, la valutazione di una chiamata di funzione produce un valore . Il valore è sempre un oggetto (a meno che non sia una funzione nulla). La distinzione è se il valore è un valore o un valore - che è determinato dal fatto che il tipo di ritorno dichiarato sia o meno un riferimento.
Kerrek SB

Risposte:


107

Non voglio restituire un valore copiato perché è inefficiente

Provalo.

Cerca RVO e NRVO e in C ++ 0x move-semantics. Nella maggior parte dei casi in C ++ 03, un parametro out è solo un buon modo per rendere il tuo codice brutto, e in C ++ 0x ti faresti davvero del male usando un parametro out.

Basta scrivere codice pulito, restituire per valore. Se le prestazioni sono un problema, descrivile (smetti di indovinare) e trova cosa puoi fare per risolverle. Probabilmente non restituirà le cose dalle funzioni.


Detto questo, se sei pronto a scrivere in quel modo, probabilmente vorrai fare il parametro out. Evita l'allocazione dinamica della memoria, che è più sicura e generalmente più veloce. Richiede che tu abbia un modo per costruire l'oggetto prima di chiamare la funzione, il che non ha sempre senso per tutti gli oggetti.

Se si desidera utilizzare l'allocazione dinamica, il minimo che si può fare è inserirlo in un puntatore intelligente. (Questo dovrebbe essere fatto sempre in ogni caso) Quindi non ti preoccupare di eliminare nulla, le cose sono eccezionalmente sicure, ecc. L'unico problema è che probabilmente è più lento del ritorno in base al valore comunque!


10
@phunehehe: nessun punto sta speculando, dovresti profilare il tuo codice e scoprirlo. (Suggerimento: no.) I compilatori sono molto intelligenti, non dovranno perdere tempo a copiare le cose in giro, se non è necessario. Anche se la copia costa qualcosa, dovresti comunque cercare un buon codice su un codice veloce; un buon codice è facile da ottimizzare quando la velocità diventa un problema. Non ha senso rovinare il codice per qualcosa che non hai idea è un problema; specialmente se lo rallenti davvero o non ne ottieni nulla. E se stai usando C ++ 0x, la semantica di spostamento rende questo un problema.
GManNickG,

1
@GMan, re: RVO: in realtà questo è vero solo se il tuo chiamante e la tua chiamata si trovano nella stessa unità di compilazione, che nel mondo reale non è il più delle volte. Quindi, sei deluso se il tuo codice non è tutto basato su modelli (nel qual caso sarà tutto in un'unica unità di compilazione) o hai in atto un'ottimizzazione del tempo di collegamento (GCC ha solo da 4.5).
Alex B,

2
@Alex: i compilatori stanno migliorando sempre di più nell'ottimizzazione tra le unità di traduzione. (VC lo fa per diverse versioni ora.)
sbi,

9
@Alex B: questa è spazzatura completa. Molte convenzioni di chiamata molto comuni rendono il chiamante responsabile per l'allocazione dello spazio per valori di ritorno di grandi dimensioni e la chiamata responsabile per la loro costruzione. RVO funziona felicemente tra le unità di compilazione anche senza ottimizzazioni del tempo di collegamento.
CB Bailey,

6
@Charles, dopo averlo verificato sembra essere corretto! Ritiro la mia dichiarazione chiaramente male informata.
Alex B,

41

Basta creare l'oggetto e restituirlo

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

Penso che ti farai un favore se dimentichi l'ottimizzazione e scrivi solo codice leggibile (dovrai eseguire un profiler in seguito, ma non pre-ottimizzare).


2
Thing thing();dichiara una funzione locale e restituisce a Thing.
dreamlax,

2
La cosa () dichiara una funzione che restituisce una cosa. Non esiste un oggetto Cosa costruito nel tuo corpo di funzione.
CB Bailey,

@dreamlax @Charles @GMan Un po 'in ritardo, ma risolto.
Amir Rachum,

Come funziona in C ++ 98? Ricevo errori sull'interprete CINT e mi chiedevo che fosse dovuto a C ++ 98 o CINT stesso ...!
xcorat,

16

Restituisci un oggetto come questo:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Questo invocherà il costruttore di copie su Things, quindi potresti voler fare la tua implementazione. Come questo:

Thing(const Thing& aThing) {}

Questo potrebbe comportare un po 'più lento, ma potrebbe non essere affatto un problema.

Aggiornare

Il compilatore probabilmente ottimizzerà la chiamata al costruttore della copia, quindi non ci saranno costi aggiuntivi. (Come ha sottolineato dreamlax nel commento).


9
Thing thing();dichiara una funzione locale che restituisce un Thing, inoltre, lo standard consente al compilatore di omettere il costruttore della copia nel caso presentato; qualsiasi compilatore moderno probabilmente lo farà.
dreamlax,

1
È utile sottolineare l'implementazione del costruttore di copie, soprattutto se è necessaria una copia approfondita.
mbadawi23,

+1 per indicare esplicitamente il costruttore di copie, anche se come dice @dreamlax molto probabilmente il compilatore "ottimizzerà" il codice di ritorno per le funzioni evitando una chiamata non necessaria al costruttore di copie.
jose.angel.jimenez,

Nel 2018, in VS 2017, sta cercando di utilizzare il costruttore di mosse. Se il costruttore di spostamenti viene eliminato e il costruttore di copie non lo è, non verrà compilato.
Andrew,

11

Hai provato a usare i puntatori intelligenti (se Thing è davvero un oggetto grande e pesante), come auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}

4
auto_ptrs sono deprecati; usa shared_ptro unique_ptrinvece.
MBraedley,

Lo aggiungerò solo qui ... Uso c ++ da anni, anche se non in modo professionale con c ++ ... Ho deciso di provare a non utilizzare più i puntatori intelligenti, sono solo un imo casino assoluto e causano tutto una serie di problemi, non contribuendo a velocizzare molto il codice. Preferirei semplicemente copiare i dati e gestire i puntatori da solo, usando RAII. Quindi ti consiglio di evitare i puntatori intelligenti, se puoi.
Andrew,

8

Un modo rapido per determinare se viene chiamato un costruttore di copie è aggiungere la registrazione al costruttore di copie della classe:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Chiama someFunction; il numero di righe "Copia costruttore è stato chiamato" che otterrai varierà tra 0, 1 e 2. Se non ne ottieni, il tuo compilatore ha ottimizzato il valore restituito (cosa che è permesso fare). Se ottieni non ottieni 0 e il tuo costruttore di copie è ridicolmente costoso, quindi cerca modi alternativi per restituire istanze dalle tue funzioni.


1

Innanzitutto hai un errore nel codice, intendi avere Thing *thing(new Thing());e solo return thing;.

  • Usa shared_ptr<Thing>. Deref come se fosse un puntatore. Verrà eliminato per te quando l'ultimo riferimento al Thingcontenuto non rientra nell'ambito.
  • La prima soluzione è molto comune nelle librerie ingenue. Ha delle prestazioni e un sovraccarico sintattico, evitalo se possibile
  • Utilizzare la seconda soluzione solo se è possibile garantire che non vengano generate eccezioni o quando le prestazioni sono assolutamente critiche (l'interfacciamento con C o l'assemblaggio prima ancora che diventi rilevante).

0

Sono sicuro che un esperto di C ++ troverà una risposta migliore, ma personalmente mi piace il secondo approccio. L'uso dei puntatori intelligenti aiuta a risolvere il problema dell'oblio deletee come dici, sembra più pulito che dover creare un oggetto prima mano (e comunque doverlo eliminare se vuoi allocarlo sull'heap).

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.