Rückgabe eines const-Verweises auf ein Objekt anstelle einer Kopie

73

Beim Umgestalten von Code bin ich auf einige Getter-Methoden gestoßen, die einen std :: string zurückgeben. So etwas zum Beispiel:

class foo
{
private:
    std::string name_;
public:
    std::string name()
    {
        return name_;
    }
};

Sicherlich würde der Getter besser ein const std::string&? Die aktuelle Methode gibt eine Kopie zurück, die nicht so effizient ist. Würde die Rückgabe einer const-Referenz stattdessen Probleme verursachen?

rauben
quelle
Wer sagte, dass es nicht effizient war. Es wurde viel Arbeit an std :: string geleistet, um diesen Vorgang effizient zu gestalten. Es gibt einen Unterschied zwischen dem Übergeben einer Referenz und einem Strincg, aber der tatsächliche Unterschied ist normalerweise unbedeutend.
Martin York
11
@Loki: Nichts sagt, dass es effizient sein wird, und tatsächlich sagt die neueste Version von C ++, dass es wahrscheinlich sein wird. Die einzige Optimierung, die diesen Code einigermaßen effizient machen würde, wäre ein std :: string mit Referenzzählung. Viele STL-Implementierungen führen jedoch keine Referenzzählung durch, da es bei modernen Multicore-CPUs viel langsamer ist, Referenzzählungen zu verwalten, als Sie vielleicht denken. Ja, die Rückgabe einer Kopie ist viel langsamer. Weder GCCs noch Microsofts STL haben in vielen Jahren Referenzzählungen auf std :: string durchgeführt.
Sean Middleditch
3
@ Seanmiddleditch: Messen Sie immer, bevor Sie annehmen (das ist alles, was ich sage). Es gibt auch viele andere Optimierungen, die der Compiler anwenden kann: Inlining und Kopierelision wären zwei, die die Kosten auf Null reduzieren.
Martin York
4
@Loki: Ja, das ist ein guter Rat. immer Profil. Ein guter Rat ist auch, den Unterschied zwischen "Annahme" und "Tatsache" zu kennen. Ich garantiere Ihnen, dass Robs Code bei keiner Implementierung eines konformen C ++ - Compilers zu einem Overhead von Null führen kann. Die Sprache schreibt vor, dass der Kopierkonstruktor bei der Rückkehr von foo :: name () mindestens einmal aufgerufen werden muss, auch bei Inlining (hat keine Auswirkung auf das Aufrufen von Konstruktoren), Kopierelision (gilt nicht für diesen Code) und RVO ( reduziert möglicherweise zusätzliche Aufrufe von Kopierkonstruktoren, beseitigt jedoch nicht alle).
Sean Middleditch
3
@Loki: Ich habe versucht, es sowohl auf GCC 4.6 als auch auf VC 10 zu messen, beide mit aktivierter maximaler Optimierung, und habe die gleichen Ergebnisse erzielt: Der Kopierkonstruktor wird aufgerufen. Dies ist das "offensichtliche" Ergebnis, selbst wenn Sie die Als-ob-Regel berücksichtigen, da der Kopierkonstruktor von std :: string tatsächlich Nebenwirkungen hat: Er erstellt entweder ein ganz neues Zeichenarray und eine Kopie der Zeichenfolge oder manipuliert sie der Referenzzählwert (abhängig von der Implementierung). Beachten Sie, dass ein ausreichend einfacher Mikro-Benchmark auf einem ref-gezählten Gerät hier möglicherweise keine "echten" Ergebnisse liefert, auf die ich bei meinem ersten Versuch bei GCC gestoßen bin.
Sean Middleditch

Antworten:

58

Dies kann nur dann zu einem Problem führen, wenn der Aufrufer die Referenz speichert, anstatt die Zeichenfolge zu kopieren, und versucht, sie zu verwenden, nachdem das Objekt zerstört wurde. So was:

foo *pFoo = new foo;
const std::string &myName = pFoo->getName();
delete pFoo;
cout << myName;  // error! dangling reference

Da Ihre vorhandene Funktion jedoch eine Kopie zurückgibt, würden Sie keinen der vorhandenen Codes beschädigen.

Bearbeiten: Modernes C ++ (dh C ++ 11 und höher) unterstützt die Rückgabewertoptimierung , sodass das Zurückgeben von Dingen nach Wert nicht länger verpönt ist. Man sollte immer noch darauf achten, extrem große Objekte nach Wert zurückzugeben, aber in den meisten Fällen sollte es in Ordnung sein.

Dima
quelle
Würde den vorhandenen Code nicht beschädigen, aber keine Leistungsvorteile erzielen. Wenn Probleme so leicht zu erkennen wären wie in Ihrem Beispiel, wäre das in Ordnung, aber es kann oft viel schwieriger sein. Zum Beispiel, wenn Sie den Vektor <foo> und push_back haben, was zu einer Größenänderung usw. führt Optimierung usw. usw.
tfinniga
7
@Dima: In diesem Fall führen die meisten Compiler wahrscheinlich eine Optimierung des Rückgabewerts für benannte Werte durch.
Richard Corden
1
@ Richard: Ja, darüber habe ich kürzlich gelesen. Dies gilt jedoch nur für den Fall, dass Sie den zurückgegebenen Wert sofort etwas zuweisen, oder? Was ist, wenn der zurückgegebene Wert an eine Funktion übergeben wird? Wird der Bau des Provisoriums noch wegoptimiert?
Dima
1
Wie lautete das endgültige Urteil? Besser eine Referenz zurückgeben oder einfach die Kopie weitergeben? Nehmen wir an, der Compiler ist dumm und ein Programmierer muss tatsächlich spezifisch und nachdenklich sein wie damals, bevor wir faul wurden :)
Volte
2
@ Dale, ich würde empfehlen, eine konstante Referenz zurückzugeben.
Dima
32

Tatsächlich ist ein weiteres Problem speziell bei der Rückgabe einer Zeichenfolge, die nicht als Referenz dient, die Tatsache, dass über die c_str () -Methode std::stringüber einen Zeiger auf eine interne Zeichenfolge zugegriffen werden kann. Dies hat mir viele Stunden lang Probleme beim Debuggen verursacht. Nehmen wir zum Beispiel an, ich möchte den Namen von foo erhalten und an JNI übergeben, um einen Jstring zu erstellen, der später an Java übergeben wird. Dabei wird eine Kopie und keine Referenz zurückgegeben. Ich könnte so etwas schreiben:const char*name()

foo myFoo = getFoo(); // Get the foo from somewhere.
const char* fooCName = foo.name().c_str(); // Woops!  foo.name() creates a temporary that's destructed as soon as this line executes!
jniEnv->NewStringUTF(fooCName);  // No good, fooCName was released when the temporary was deleted.

Wenn Ihr Aufrufer so etwas tun wird, ist es möglicherweise besser, einen intelligenten Zeiger oder eine konstante Referenz zu verwenden oder zumindest einen bösen Warnkommentar-Header über Ihrer foo.name () -Methode zu haben. Ich erwähne JNI, weil ehemalige Java-Codierer möglicherweise besonders anfällig für diese Art der Methodenverkettung sind, die ansonsten harmlos erscheint.

Oger Psalm33
quelle
3
Verdammt, guter Fang. Obwohl ich denke, dass C ++ 11 die Lebensdauer von Provisorien verkürzt hat, um diesen Code für jeden kompatiblen Compiler gültig zu machen.
Mark McKenna
@ MarkMcKenna Das ist gut zu wissen. Ich habe ein wenig gesucht, und diese Antwort scheint darauf hinzudeuten, dass Sie auch mit C ++ 11 noch ein wenig vorsichtig sein müssen: stackoverflow.com/a/2507225/13140
Ogre Psalm33
18

Ein Problem für die Rückgabe der const-Referenz wäre, wenn der Benutzer Folgendes codiert:

const std::string & str = myObject.getSomeString() ;

Bei einer std::stringRückgabe würde das temporäre Objekt am Leben bleiben und an str angehängt, bis str den Gültigkeitsbereich verlässt.

Aber was passiert mit einem const std::string &? Ich vermute, dass wir einen konstanten Verweis auf ein Objekt haben würden, das sterben könnte, wenn sein übergeordnetes Objekt die Zuordnung aufhebt:

MyObject * myObject = new MyObject("My String") ;
const std::string & str = myObject->getSomeString() ;
delete myObject ;
// Use str... which references a destroyed object.

Meine Präferenz gilt also der Konstantenreferenzrückgabe (da ich mit dem Senden einer Referenz ohnehin nur bequemer bin als zu hoffen, dass der Compiler die zusätzliche temporäre Option optimiert), solange der folgende Vertrag eingehalten wird: "Wenn Sie ihn darüber hinaus wünschen Die Existenz meines Objekts kopieren sie vor der Zerstörung meines Objekts. "

paercebal
quelle
10

Einige Implementierungen von std :: string teilen sich den Speicher mit Copy-on-Write-Semantik, sodass Return-by-Value fast so effizient sein kann wie Return-by-Reference, und Sie müssen sich keine Gedanken über die Lebensdauer machen (die Laufzeit tut dies) es für dich).

Wenn Sie sich Sorgen um die Leistung machen, dann vergleichen Sie diese (<= kann das nicht genug betonen) !!! Probieren Sie beide Ansätze aus und messen Sie den Gewinn (oder das Fehlen davon). Wenn einer besser ist und es dich wirklich interessiert, dann benutze ihn. Wenn nicht, dann bevorzugen Sie einen Nebenwert für den Schutz, den es gegen lebenslange Probleme bietet, die von anderen Personen erwähnt wurden.

Sie wissen, was sie über Annahmen sagen ...

rlerallut
quelle
Stattdessen müssen Sie sich um die Thread-Sicherheit sorgen, wenn Sie nicht verwandte Zeichenfolgen
ändern
10
Heh. Was die Laufzeit gibt, nimmt das Multithreading weg. :)
Rlerallut
1
Aus Neugier - implementiert g ++ std :: string mit Copy-on-Write-Semantik?
Frank
4
@ user60628 COW ist in Ungnade gefallen (vor zehn Jahren) siehe diesen Artikel
Motti
COW ist in C ++ 11 und höher tatsächlich nicht zulässig.
MM
7

Okay, die Unterschiede zwischen der Rücksendung einer Kopie und der Rücksendung der Referenz sind:

  • Leistung : Die Rückgabe der Referenz kann schneller sein oder nicht. Dies hängt davon ab, wie std::stringIhre Compiler-Implementierung implementiert wird (wie andere bereits betont haben). Aber selbst wenn Sie die Referenz zurückgeben, beinhaltet die Zuweisung nach dem Funktionsaufruf normalerweise eine Kopie, wie instd::string name = obj.name();

  • Sicherheit : Die Rücksendung der Referenz kann Probleme verursachen oder nicht (baumelnde Referenz). Wenn die Benutzer Ihrer Funktion nicht wissen, was sie tun, die Referenz als Referenz speichern und verwenden, nachdem das bereitgestellte Objekt den Gültigkeitsbereich verlassen hat, liegt ein Problem vor.

Wenn Sie es schnell und sicher wollen, verwenden Sie boost :: shared_ptr . Ihr Objekt kann die Zeichenfolge intern als speichern shared_ptrund a zurückgeben shared_ptr. Auf diese Weise wird das Objekt nicht kopiert, und es ist immer sicher (es sei denn, Ihre Benutzer ziehen den Rohzeiger mit heraus get()und tun dies, nachdem Ihr Objekt den Gültigkeitsbereich verlassen hat).

Frank
quelle
4

Ich würde es ändern, um const std :: string & zurückzugeben. Der Anrufer wird wahrscheinlich trotzdem eine Kopie des Ergebnisses erstellen, wenn Sie nicht den gesamten Anrufcode ändern, dies führt jedoch zu keinen Problemen.

Eine mögliche Falte tritt auf, wenn Sie mehrere Threads haben, die name () aufrufen. Wenn Sie eine Referenz zurückgeben, aber später den zugrunde liegenden Wert ändern, ändert sich der Wert des Anrufers. Der vorhandene Code sieht jedoch ohnehin nicht threadsicher aus.

Schauen Sie sich Dimas Antwort auf ein damit verbundenes potenzielles, aber unwahrscheinliches Problem an.

Kristopher Johnson
quelle
3

Es ist denkbar, dass Sie etwas kaputt machen könnten, wenn der Anrufer wirklich eine Kopie wollte, weil er das Original ändern wollte und eine Kopie davon aufbewahren wollte. Es ist jedoch weitaus wahrscheinlicher, dass tatsächlich nur eine konstante Referenz zurückgegeben wird.

Am einfachsten ist es, es zu versuchen und dann zu testen, um festzustellen, ob es noch funktioniert, vorausgesetzt, Sie haben eine Art Test, den Sie ausführen können. Wenn nicht, würde ich mich darauf konzentrieren, zuerst den Test zu schreiben, bevor ich mit dem Refactoring fortfahre.

Airsource Ltd.
quelle
1

Die Chancen stehen gut, dass die typische Verwendung dieser Funktion nicht unterbrochen wird, wenn Sie zu einer konstanten Referenz wechseln.

Wenn der gesamte Code, der diese Funktion aufruft, unter Ihrer Kontrolle steht, nehmen Sie einfach die Änderung vor und prüfen Sie, ob sich der Compiler beschwert.

17 von 26
quelle
1

Ist das wichtig? Sobald Sie einen modernen Optimierungs-Compiler verwenden, wird für Funktionen, die nach Wert zurückgegeben werden, keine Kopie verwendet, es sei denn, dies ist semantisch erforderlich.

Lesen Sie hierzu die häufig gestellten Fragen zu C ++ lite .

christopher_f
quelle
Ist die Return-by-Value-Optimierung so ausgefeilt, dass ein Zeiger / Verweis auf den ursprünglichen Elementwert io einer Kopie verwendet wird? Ich glaube nicht, dass dies der Fall ist. Die Rbv-Optimierung überspringt einige Schritte bei der Rückgabe eines Werts, ruft jedoch weiterhin den Kopiercode von cstring auf, wenn auch die In-Place-Version.
QBziZ
Wenn die Funktion eine Kopie einer Klassenmitgliedsvariablen wie in diesem Beispiel zurückgibt, gibt sie tatsächlich eine Kopie zurück. Das ursprüngliche Mitglied kann dabei nicht zerstört werden (im Gegensatz zu einer Funktion, die nach Wert zurückgibt und beispielsweise eine temporäre Rückgabe erstellt)
MM
0

Kommt darauf an, was du tun musst. Vielleicht möchten Sie, dass alle Aufrufer den zurückgegebenen Wert ändern, ohne die Klasse zu ändern. Wenn Sie die const-Referenz zurückgeben, wird diese nicht fliegen.

Das nächste Argument ist natürlich, dass der Anrufer dann seine eigene Kopie erstellen könnte. Wenn Sie jedoch wissen, wie die Funktion verwendet wird, und wissen, dass dies trotzdem geschieht, sparen Sie sich möglicherweise einen Schritt später im Code.

Joel Coehoorn
quelle
Ich denke nicht, dass das ein Problem ist. Ein Aufrufer kann einfach "std :: string s = aFoo.name ()" ausführen, und s ist eine veränderbare Kopie.
Kristopher Johnson
0

Normalerweise gebe ich const & zurück, es sei denn, ich kann nicht. QBziZ gibt ein Beispiel dafür, wo dies der Fall ist. Natürlich behauptet QBziZ auch, dass std :: string eine Copant-on-Write-Semantik hat, was heutzutage selten der Fall ist, da COW in einer Multithread-Umgebung viel Overhead mit sich bringt. Indem Sie const & zurückgeben, müssen Sie den Anrufer dazu verpflichten, das Richtige mit der Zeichenfolge am Ende zu tun. Da es sich jedoch um Code handelt, der bereits verwendet wird, sollten Sie ihn wahrscheinlich nicht ändern, es sei denn, die Profilerstellung zeigt, dass das Kopieren dieser Zeichenfolge massive Leistungsprobleme verursacht. Wenn Sie sich dann entscheiden, es zu ändern, müssen Sie es gründlich testen, um sicherzustellen, dass Sie nichts kaputt gemacht haben. Hoffentlich machen die anderen Entwickler, mit denen Sie arbeiten, keine skizzenhaften Dinge wie in Dimas Antwort.

Brett Hall
quelle
-1

Wenn Sie einen Verweis auf ein Mitglied zurückgeben, wird die Implementierung der Klasse verfügbar gemacht. Das könnte verhindern, dass die Klasse geändert wird. Kann für private oder geschützte Methoden nützlich sein, wenn die Optimierung erforderlich ist. Was sollte ein C ++ - Getter zurückgeben?

Xavier Nguyen-Thai
quelle