2008-09-25 13 views
69

Während ich etwas Code umformatierete, stieß ich auf einige Getter-Methoden, die eine std :: string zurückgeben. So etwas wie dies zum Beispiel:Rückgabe einer const-Referenz an ein Objekt anstelle einer Kopie

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

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

+0

Wer sagte, dass es nicht effizient war. Es wurde eine Menge Arbeit an std :: string geleistet, um diese Operation effizient zu machen. Es besteht ein Unterschied zwischen der Übergabe einer Referenz und einer Strincg, aber der tatsächliche Unterschied ist normalerweise nicht signifikant. –

+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 eine Referenz-gezählte std :: string. Viele STL-Implementierungen führen jedoch keine Referenzzählung durch, da es bei modernen Multicore-CPUs viel langsamer ist, Referenzwerte zu verwalten, als Sie vielleicht denken. Ja, die Rückgabe einer Kopie ist viel langsamer. Weder die GCC noch die Microsoft STL haben in vielen Jahren Referenzzählungen für std :: string durchgeführt. –

+3

@seanmiddleditch: Immer messen, bevor Sie annehmen (das ist alles, was ich sage). Es gibt auch viele andere Optimierungen, die der Compiler anwenden kann: Inlining und Copy elision sind zwei, die die Kosten auf Null reduzieren. –

Antwort

51

Die einzige Möglichkeit, die ein Problem verursachen kann, ist, wenn der Aufrufer die Referenz speichert, anstatt die Zeichenfolge zu kopieren, und versucht, sie zu verwenden, nachdem das Objekt zerstört wurde. Wie folgt aus:

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

Da jedoch Ihre vorhandenen Funktion eine Kopie zurückgibt, dann würden Sie keine der vorhandenen Code brechen.

+0

würde vorhandenen Code nicht brechen, aber _yan_ Leistungsvorteile nicht erhalten. Wenn Probleme so einfach zu erkennen sind wie in Ihrem Beispiel, wäre das in Ordnung, aber es kann oft viel komplizierter sein .. zum Beispiel, wenn Sie Vektor haben , und push_back, verursacht eine Größenänderung, etc ..vorzeitige Optimierung, etc, etc. – tfinniga

+0

Natürlich erhalten Sie einen Leistungsvorteil. Wenn Sie das Objekt zurückgeben, zahlen Sie die Kosten für die Erstellung einer zusätzlichen Kopie des Objekts, des temporären, das zurückgegeben wird. Sie bezahlen diese Kosten nicht, wenn Sie eine Referenz zurückgeben. – Dima

+6

@Dima: Es ist wahrscheinlich, dass die meisten Compiler in diesem Fall eine Wertoptimierung für den benannten Wert ausführen. –

4

Ich würde es ändern, um const std :: string & zurückzugeben. Der Anrufer wird wahrscheinlich trotzdem eine Kopie des Ergebnisses machen, wenn Sie nicht den gesamten Anrufcode ändern, aber es wird keine Probleme verursachen.

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

Werfen Sie einen Blick auf Dimas Antwort auf ein verwandtes potentielles, aber unwahrscheinliches Problem.

0

Hängt davon ab, was Sie tun müssen. Vielleicht möchten Sie allen Aufrufenden den zurückgegebenen Wert ändern, ohne die Klasse zu ändern. Wenn Sie die const-Referenz zurückgeben, die nicht fliegen wird.

Natürlich ist das nächste Argument, dass der Anrufer dann ihre eigene Kopie erstellen könnte. Aber wenn Sie wissen, wie die Funktion verwendet wird, und Sie wissen, dass dies trotzdem geschieht, können Sie dadurch einen Schritt später im Code sparen.

+0

Ich glaube nicht, dass das ein Problem ist. Ein Aufrufer kann einfach "std :: string s = aFoo.name()" ausführen, und s wird eine veränderbare Kopie sein. –

3

Es ist denkbar, dass Sie etwas kaputt machen könnten, wenn der Aufrufer wirklich eine Kopie wollte, weil sie das Original ändern wollten und eine Kopie davon behalten wollten. Es ist jedoch viel wahrscheinlicher, dass es tatsächlich eine konstante Referenz zurückgibt.

Am einfachsten ist es, es zu versuchen und dann zu testen, 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.

1

Die Chancen sind ziemlich gut, dass die typische Verwendung dieser Funktion nicht bricht, wenn Sie zu einer const-Referenz wechseln.

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

0

Ich normalerweise zurück const &, wenn ich nicht kann. QBziZ gibt ein Beispiel dafür, wo dies der Fall ist. Natürlich behauptet QBziZ auch, dass std :: string eine Copy-on-Write-Semantik hat, was heutzutage selten der Fall ist, da COW in einer Multi-Threaded-Umgebung viel Aufwand verursacht. Indem Sie const & zurückgeben, geben Sie dem Aufrufer die Pflicht, mit der Zeichenfolge an ihrem Ende das Richtige zu tun. Da Sie jedoch mit Code arbeiten, der bereits verwendet wird, sollten Sie ihn wahrscheinlich nicht ändern, es sei denn, das Profiling zeigt, dass das Kopieren dieser Zeichenfolge massive Leistungsprobleme verursacht. Wenn Sie sich entscheiden, es zu ändern, müssen Sie gründlich testen, um sicherzustellen, dass Sie nichts kaputt gemacht haben. Hoffentlich machen die anderen Entwickler, mit denen du arbeitest, keine skizzenhaften Sachen wie in Dimas Antwort.

2

Spielt es eine Rolle? Sobald Sie einen modernen optimierenden Compiler verwenden, werden Funktionen, die wertmäßig zurückgegeben werden, keine Kopie enthalten, außer wenn sie semantisch dazu benötigt werden.

Siehe hierzu the C++ lite FAQ.

+0

Ist die Rückgabewert-Optimierung so hochentwickelt, dass sie einen Zeiger/Verweis auf den ursprünglichen Elementwert i.o. von einer Kopie? Ich denke nicht, dass das der Fall ist. Die Rbv-Optimierung überspringt beim Zurückgeben eines Werts ein paar Schritte, aber sie würde immer noch den copy-ctor von cstring aufrufen, wenn auch die In-Place-Version. – QBziZ

16

Ein Problem für die konstante Referenz Rückkehr wäre, wenn der Benutzer codiert so etwas wie:

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

Mit einem std::string Gegenzug würde das temporäre Objekt am Leben bleiben und an str bis str den Gültigkeitsbereich verlässt.

Aber was passiert mit einem const std::string &? Meine Vermutung ist, dass wir eine konstante Referenz auf ein Objekt haben würde, die, wenn das Objekt seine Eltern sterben könnte es freigibt:

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

So meine Vorliebe geht an die konstante Referenz return (weil, wie auch immer, ich bin nur mehr confortable mit dem Senden einer Referenz als die Hoffnung, dass der Compiler wird die zusätzliche temporäre optimieren), solange der folgende Vertrag eingehalten wird: "Wenn Sie es über die Existenz meines Objekts wollen, kopieren sie es vor der Zerstörung meines Objekts"

10

Einige Implementierungen von Std :: String share Speicher mit Copy-on-Write-Semantik, so kann Return-by-Wert fast so effizient sein wie Return-by-Referenz und Sie nicht müssen Achten Sie auf die Lebensdauerprobleme (die Laufzeit tut es für Sie).

Wenn Sie über die Leistung besorgt sind, dann Benchmark es (< = kann nicht genug betonen) !!! Probieren Sie beide Ansätze aus und messen Sie die Verstärkung (oder deren Fehlen). Wenn einer besser ist und es dir wirklich wichtig ist, dann benutze ihn. Wenn nicht, dann bevorzugen Sie den Wert für den Schutz, den es gegen lebenslange Probleme anbietet, die von anderen Personen erwähnt werden.

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

+0

Stattdessen müssen Sie sich um die Threadsicherheit kümmern, wenn Sie nicht zusammenhängende Zeichenfolgen ändern :) – bk1e

+10

Heh. Was die Laufzeit gibt, nimmt das Multithreading weg. :) – rlerallut

+1

Aus Neugier - implementiert g ++ std :: string mit Copy-on-Write-Semantik? – Frank

7

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

  • Leistung: die Referenzzurück kann oder darf nicht schneller sein; Es hängt davon ab, wie std::string durch Ihre Compilerimplementierung implementiert wird (wie andere darauf hingewiesen haben). Aber auch wenn Sie die Referenz die Zuordnung zurück, nachdem der Funktionsaufruf in der Regel eine Kopie handelt, wie in std::string name = obj.name();

  • Sicherheit: die Referenz Zurückgeben kann oder auch nicht zu Problemen führen (baumelnden Referenz). Wenn die Benutzer Ihrer Funktion nicht wissen, was sie tun, wird die Referenz als Referenz gespeichert und verwendet, nachdem das bereitstellende Objekt den Gültigkeitsbereich verlassen hat. Dann liegt ein Problem vor.

Wenn Sie es schnell und sicher Verwendung boost :: shared_ptr. Ihr Objekt kann die Zeichenfolge intern als shared_ptr speichern und eine shared_ptr zurückgeben. Auf diese Weise wird das Objekt nicht kopiert und es ist immer sicher (es sei denn, Ihre Benutzer ziehen den rohen Zeiger mit get() heraus und machen damit etwas, nachdem Ihr Objekt den Geltungsbereich verlässt).

+4

hinweis: seit C++ 11 gibt es 'std :: shared_ptr' –

25

Eigentlich Ein weiteres Problem speziell mit einer Schnur nicht durch Bezugnahme zurückkehrt, ist die Tatsache, dass std::string Zugriff über Zeiger liefert an einen internen const char* über das c_str() Methode. Das hat mich viele Stunden des Debugging-Kopfschmerzes verursacht. Nehmen wir zum Beispiel an, ich möchte den Namen von foo erhalten und ihn an JNI übergeben, um später ein jstring für die Übergabe in Java zu erstellen, und name() gibt eine Kopie und keine Referenz zurück. Ich könnte so etwas schreiben:

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 Anrufer tun diese Art der Sache werden wird, könnte es besser sein, eine Art von intelligenten Zeiger zu verwenden, oder eine konstante Referenz, oder zumindest haben ein fieser Warnkommentar-Header über Ihre foo.name() -Methode. Ich erwähne JNI, weil frühere Java-Programmierer besonders anfällig für diese Art von Methodenverkettung sind, die ansonsten harmlos erscheinen könnte.

+2

Verdammt, guter Fang. Obwohl ich denke, C++ 11 hat die Lebensdauer von Provisorien festgelegt, um diesen Code für jeden kompatiblen Compiler gültig zu machen. –

+0

@MarkMcKenna Das ist gut zu wissen. Ich habe ein wenig gesucht, und diese Antwort scheint darauf hinzuweisen, dass Sie immer noch etwas vorsichtig sein müssen, sogar mit C++ 11: http://stackoverflow.com/a/2507225/13140 –

0

Die Rückgabe eines Verweises auf ein Mitglied macht die Implementierung der Klasse verfügbar. Das könnte verhindern, die Klasse zu ändern. Kann für private oder geschützte Methoden nützlich sein, wenn die Optimierung benötigt wird. What should a C++ getter return