Wie kann man in C ++ ein Objekt zurückgeben?

166

Ich weiß, dass der Titel bekannt vorkommt, da es viele ähnliche Fragen gibt, aber ich frage nach einem anderen Aspekt des Problems (ich kenne den Unterschied zwischen dem Ablegen von Dingen auf dem Stapel und dem Ablegen auf dem Haufen).

In Java kann ich immer Verweise auf "lokale" Objekte zurückgeben

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

In C ++ habe ich zwei Möglichkeiten, um etwas Ähnliches zu tun

(1) Ich kann Referenzen verwenden, wenn ich ein Objekt "zurückgeben" muss

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

Dann benutze es so

Thing thing;
calculateThing(thing);

(2) Oder ich kann einen Zeiger auf ein dynamisch zugewiesenes Objekt zurückgeben

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

Dann benutze es so

Thing* thing = calculateThing();
delete thing;

Mit dem ersten Ansatz muss ich den Speicher nicht manuell freigeben, aber für mich ist der Code schwer zu lesen. Das Problem mit dem zweiten Ansatz ist, dass ich mich daran erinnern muss delete thing;, was nicht ganz gut aussieht. Ich möchte keinen kopierten Wert zurückgeben, weil er ineffizient ist (glaube ich). Hier kommen die Fragen

  • Gibt es eine dritte Lösung (für die kein Kopieren des Werts erforderlich ist)?
  • Gibt es ein Problem, wenn ich mich an die erste Lösung halte?
  • Wann und warum sollte ich die zweite Lösung verwenden?
Phunehehe
quelle
31
+1 für die nette Frage.
Kangkan
1
Um sehr pedantisch zu sein, ist es etwas ungenau zu sagen, dass "Funktionen etwas zurückgeben". Richtiger ergibt die Auswertung eines Funktionsaufrufs einen Wert . Der Wert ist immer ein Objekt (es sei denn, es handelt sich um eine ungültige Funktion). Die Unterscheidung besteht darin, ob der Wert ein gl-Wert oder ein pr-Wert ist. Dies wird dadurch bestimmt, ob der deklarierte Rückgabetyp eine Referenz ist oder nicht.
Kerrek SB

Antworten:

107

Ich möchte keinen kopierten Wert zurückgeben, da dieser ineffizient ist

Beweise es.

Suchen Sie nach RVO und NRVO sowie in C ++ 0x Verschiebungssemantik. In den meisten Fällen in C ++ 03 ist ein out-Parameter nur ein guter Weg, um Ihren Code hässlich zu machen, und in C ++ 0x würden Sie sich tatsächlich selbst verletzen, wenn Sie einen out-Parameter verwenden.

Schreiben Sie einfach sauberen Code und geben Sie ihn nach Wert zurück. Wenn die Leistung ein Problem darstellt, profilieren Sie es (hören Sie auf zu raten) und finden Sie heraus, wie Sie es beheben können. Es wird wahrscheinlich keine Dinge von Funktionen zurückgeben.


Das heißt, wenn Sie fest entschlossen sind, so zu schreiben, möchten Sie wahrscheinlich den out-Parameter ausführen. Es vermeidet eine dynamische Speicherzuweisung, die sicherer und im Allgemeinen schneller ist. Es erfordert eine Möglichkeit, das Objekt vor dem Aufrufen der Funktion zu erstellen, was nicht immer für alle Objekte sinnvoll ist.

Wenn Sie die dynamische Zuordnung verwenden möchten, können Sie sie zumindest in einen intelligenten Zeiger einfügen. (Dies sollte sowieso die ganze Zeit gemacht werden.) Dann müssen Sie sich keine Gedanken über das Löschen von Dingen machen, die Dinge sind ausnahmesicher usw. Das einzige Problem ist, dass es wahrscheinlich langsamer ist als die Rückgabe nach Wert!

GManNickG
quelle
10
@phunehehe: Es macht keinen Sinn zu spekulieren, du solltest deinen Code profilieren und herausfinden. (Hinweis: Nein.) Compiler sind sehr schlau, sie werden keine Zeit damit verschwenden, Dinge zu kopieren, wenn sie nicht müssen. Selbst wenn das Kopieren etwas kostet, sollten Sie dennoch nach gutem Code über schnellem Code streben. Guter Code lässt sich leicht optimieren, wenn Geschwindigkeit zum Problem wird. Es macht keinen Sinn, Code für etwas zu hässlich zu machen, von dem Sie keine Ahnung haben, dass es ein Problem ist. vor allem, wenn Sie es tatsächlich verlangsamen oder nichts daraus machen. Wenn Sie C ++ 0x verwenden, ist dies aufgrund der Verschiebungssemantik kein Problem.
GManNickG
1
@GMan, re: RVO: Eigentlich ist dies nur dann der Fall, wenn sich Ihr Anrufer und Ihr Angerufene zufällig in derselben Kompilierungseinheit befinden, was in der realen Welt meistens nicht der Fall ist. Sie werden also enttäuscht sein, wenn Ihr Code nicht alle Vorlagen enthält (in diesem Fall befindet sich alles in einer Kompilierungseinheit) oder Sie eine Optimierung der Verbindungszeit eingerichtet haben (GCC hat sie nur ab 4.5).
Alex B
2
@Alex: Compiler können immer besser über Übersetzungseinheiten hinweg optimieren. (VC macht es jetzt für mehrere Veröffentlichungen.)
sbi
9
@ Alex B: Dies ist kompletter Müll. Viele sehr verbreitete Anrufkonventionen machen den Anrufer für die Zuweisung von Speicherplatz für große Rückgabewerte und den Angerufenen für deren Erstellung verantwortlich. RVO funktioniert auch ohne Optimierung der Verbindungszeit problemlos über Kompilierungseinheiten hinweg.
CB Bailey
6
@ Charles, nach Überprüfung scheint es richtig zu sein! Ich ziehe meine eindeutig falsch informierte Aussage zurück.
Alex B
41

Erstellen Sie einfach das Objekt und geben Sie es zurück

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

Ich denke, Sie tun sich selbst einen Gefallen, wenn Sie die Optimierung vergessen und nur lesbaren Code schreiben (Sie müssen später einen Profiler ausführen - aber nicht voroptimieren).

Amir Rachum
quelle
2
Thing thing();deklariert eine lokale Funktion und gibt a zurück Thing.
Dreamlax
2
Thing thing () deklariert eine Funktion, die eine Sache zurückgibt. In Ihrem Funktionskörper ist kein Thing-Objekt aufgebaut.
CB Bailey
@dreamlax @Charles @GMan Etwas spät, aber behoben.
Amir Rachum
Wie funktioniert das in C ++ 98? Ich erhalte Fehler beim CINT-Interpreter und habe mich gefragt, ob dies an C ++ 98 oder CINT selbst liegt ...!
Xcorat
16

Geben Sie einfach ein Objekt wie das folgende zurück:

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

Dadurch wird der Kopierkonstruktor für Things aufgerufen. Sie können dies also selbst implementieren. So was:

Thing(const Thing& aThing) {}

Dies kann etwas langsamer sein, ist aber möglicherweise überhaupt kein Problem.

Aktualisieren

Der Compiler wird wahrscheinlich den Aufruf des Kopierkonstruktors optimieren, sodass kein zusätzlicher Aufwand entsteht. (Wie Dreamlax im Kommentar ausgeführt hat).

Martin Ingvar Kofoed Jensen
quelle
9
Thing thing();deklariert eine lokale Funktion Thing, die a zurückgibt. Außerdem erlaubt der Standard dem Compiler, den Kopierkonstruktor in dem von Ihnen vorgestellten Fall wegzulassen. Jeder moderne Compiler wird es wahrscheinlich tun.
Dreamlax
1
Sie bringen einen guten Punkt für die Implementierung des Kopierkonstruktors, insbesondere wenn eine tiefe Kopie benötigt wird.
mbadawi23
+1 für die explizite Angabe des Kopierkonstruktors, obwohl der Compiler, wie @dreamlax sagt, höchstwahrscheinlich den Rückgabecode für die Funktionen "optimieren" wird, um einen nicht wirklich notwendigen Aufruf des Kopierkonstruktors zu vermeiden.
Jose.angel.jimenez
Im Jahr 2018, in VS 2017, wird versucht, den Verschiebungskonstruktor zu verwenden. Wenn der Verschiebungskonstruktor gelöscht wird und der Kopierkonstruktor nicht, wird er nicht kompiliert.
Andrew
11

Haben Sie versucht, intelligente Zeiger zu verwenden (wenn Thing wirklich ein großes und schweres Objekt ist), wie 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 
}
Demetrios
quelle
4
auto_ptrs sind veraltet; benutze shared_ptroder unique_ptrstattdessen.
MBraedley
Ich werde das hier nur hinzufügen ... Ich benutze C ++ seit Jahren, wenn auch nicht professionell mit C ++ ... Ich habe mich entschlossen, keine intelligenten Zeiger mehr zu verwenden, sie sind nur ein absolutes Chaos imo und verursachen alles Arten von Problemen, die nicht wirklich dazu beitragen, den Code zu beschleunigen. Ich würde so viel lieber Daten kopieren und Zeiger selbst mit RAII verwalten. Ich rate daher, wenn Sie können, intelligente Zeiger zu vermeiden.
Andrew
8

Eine schnelle Möglichkeit, um festzustellen, ob ein Kopierkonstruktor aufgerufen wird, besteht darin, dem Kopierkonstruktor Ihrer Klasse eine Protokollierung hinzuzufügen:

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

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

Rufen Sie an someFunction; Die Anzahl der Zeilen "Kopierkonstruktor wurde aufgerufen", die Sie erhalten, variiert zwischen 0, 1 und 2. Wenn Sie keine erhalten, hat Ihr Compiler den Rückgabewert optimiert (was zulässig ist). Wenn Sie nicht 0 erhalten Sie erhalten, und Ihr Copykonstruktor ist unverschämt teuer, dann nach alternativen Möglichkeiten suchen Instanzen von Ihren Funktionen zurückzukehren.

Dreamlax
quelle
1

Erstens haben Sie einen Fehler im Code, den Sie haben wollen Thing *thing(new Thing());, und nur return thing;.

  • Verwenden Sie shared_ptr<Thing>. Deref es als ein Zeiger. Es wird für Sie gelöscht, wenn der letzte Verweis auf das ThingEnthaltene nicht mehr gültig ist.
  • Die erste Lösung ist in naiven Bibliotheken sehr verbreitet. Es hat eine gewisse Leistung und einen syntaktischen Overhead. Vermeiden Sie dies, wenn möglich
  • Verwenden Sie die zweite Lösung nur, wenn Sie garantieren können, dass keine Ausnahmen ausgelöst werden oder wenn die Leistung absolut kritisch ist (Sie werden eine Schnittstelle zu C oder Assembly herstellen, bevor dies überhaupt relevant wird).
Matt Joiner
quelle
0

Ich bin sicher, dass ein C ++ - Experte eine bessere Antwort finden wird, aber ich persönlich mag den zweiten Ansatz. Die Verwendung intelligenter Zeiger hilft bei dem Problem, zu vergessen, deleteund wie Sie sagen, sieht es sauberer aus, als ein Objekt zuvor erstellen zu müssen (und es trotzdem löschen zu müssen, wenn Sie es auf dem Heap zuweisen möchten).

EMP
quelle