Wenn ich eine Funktion habe, die mit einem arbeiten muss shared_ptr
, wäre es nicht effizienter, ihr einen Verweis darauf zu übergeben (um das Kopieren des shared_ptr
Objekts zu vermeiden )? Was sind die möglichen schlimmen Nebenwirkungen? Ich stelle mir zwei mögliche Fälle vor:
1) Innerhalb der Funktion wird eine Kopie des Arguments erstellt, wie in
ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)
{
...
m_sp_member=sp; //This will copy the object, incrementing refcount
...
}
2) Innerhalb der Funktion wird nur das Argument verwendet, wie in
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
Ich kann in beiden Fällen keinen guten Grund sehen, das zu bestehen boost::shared_ptr<foo>
by-Wert anstelle des Verweises zu übergeben. Das Übergeben eines Werts würde den Referenzzähler aufgrund des Kopierens nur "vorübergehend" erhöhen und ihn dann beim Verlassen des Funktionsumfangs verringern. Übersehe ich etwas?
Nur um zu verdeutlichen, nachdem ich mehrere Antworten gelesen habe: Ich stimme den Bedenken hinsichtlich vorzeitiger Optimierung vollkommen zu und versuche immer, zuerst das Profil zu erstellen und dann an den Hotspots zu arbeiten. Meine Frage war eher aus rein technischer Code-Sicht, wenn Sie wissen, was ich meine.
quelle
Antworten:
Der Zweck einer bestimmten
shared_ptr
Instanz besteht darin, (so weit wie möglich) zu gewährleistenshared_ptr
, dass das Objekt, auf das sie zeigt, solange es im Geltungsbereich liegt, weiterhin vorhanden ist, da seine Referenzanzahl mindestens 1 beträgt.shared_ptr
Wenn Sie also einen Verweis auf a verwenden , deaktivieren Sie diese Garantie. Also in Ihrem zweiten Fall:Woher wissen Sie, dass dies
sp->do_something()
aufgrund eines Nullzeigers nicht explodiert?Es hängt alles davon ab, was sich in diesen '...' Abschnitten des Codes befindet. Was ist, wenn Sie während des ersten "..." etwas aufrufen, das den Nebeneffekt (irgendwo in einem anderen Teil des Codes) hat, ein
shared_ptr
Objekt für dasselbe Objekt zu löschen? Und was ist, wenn es das einzige istshared_ptr
, das sich von diesem Objekt unterscheidet? Bye bye Objekt, genau dort, wo Sie versuchen möchten, es zu verwenden.Es gibt also zwei Möglichkeiten, diese Frage zu beantworten:
Untersuchen Sie die Quelle Ihres gesamten Programms sehr sorgfältig, bis Sie sicher sind, dass das Objekt während des Funktionskörpers nicht stirbt.
Ändern Sie den Parameter wieder in ein eindeutiges Objekt anstelle einer Referenz.
Allgemeiner Ratschlag, der hier gilt: Nehmen Sie aus Gründen der Leistung keine riskanten Änderungen an Ihrem Code vor, bis Sie Ihr Produkt in einer realistischen Situation in einem Profiler zeitlich festgelegt und abschließend gemessen haben, dass die von Ihnen gewünschte Änderung a signifikanter Unterschied zur Leistung.
Update für Kommentator JQ
Hier ist ein erfundenes Beispiel. Es ist absichtlich einfach, daher ist der Fehler offensichtlich. In realen Beispielen ist der Fehler nicht so offensichtlich, weil er in Schichten von echten Details verborgen ist.
Wir haben eine Funktion, die irgendwo eine Nachricht sendet. Es kann sich um eine große Nachricht handeln. Anstatt eine zu verwenden
std::string
, die wahrscheinlich kopiert wird, wenn sie an mehrere Stellen weitergegeben wird, verwenden wir eineshared_ptr
an eine Zeichenfolge:(Wir "senden" es für dieses Beispiel einfach an die Konsole).
Jetzt möchten wir eine Funktion hinzufügen, um die vorherige Nachricht zu speichern. Wir möchten das folgende Verhalten: Es muss eine Variable vorhanden sein, die die zuletzt gesendete Nachricht enthält. Während jedoch eine Nachricht gesendet wird, darf keine vorherige Nachricht vorhanden sein (die Variable sollte vor dem Senden zurückgesetzt werden). Also deklarieren wir die neue Variable:
Dann ändern wir unsere Funktion gemäß den von uns festgelegten Regeln:
Bevor wir mit dem Senden beginnen, verwerfen wir die aktuelle vorherige Nachricht. Nachdem der Versand abgeschlossen ist, können wir die neue vorherige Nachricht speichern. Alles gut. Hier ist ein Testcode:
Und wie erwartet wird dies gedruckt
Hi!
zweimal .Jetzt kommt Herr Maintainer, der sich den Code ansieht und denkt: Hey, dieser Parameter
send_message
ist ashared_ptr
:Das kann natürlich geändert werden in:
Denken Sie an die Leistungssteigerung, die dies bringen wird! (Es ist egal, dass wir im Begriff sind, eine normalerweise große Nachricht über einen Kanal zu senden, sodass die Leistungssteigerung so gering ist, dass sie nicht messbar ist.)
Das eigentliche Problem ist jedoch, dass der Testcode jetzt ein undefiniertes Verhalten aufweist (in Debug-Builds von Visual C ++ 2010 stürzt er ab).
Herr Maintainer ist davon überrascht, fügt jedoch eine Verteidigungskontrolle hinzu
send_message
, um das Auftreten des Problems zu stoppen:Aber natürlich geht es immer noch weiter und stürzt ab, weil
msg
es beimsend_message
Aufruf nie null ist.Wie gesagt, wenn der gesamte Code in einem trivialen Beispiel so nahe beieinander liegt, ist es leicht, den Fehler zu finden. In realen Programmen mit komplexeren Beziehungen zwischen veränderlichen Objekten, die Zeiger aufeinander enthalten, ist dies jedoch einfach zu erstellen den Fehler und die erforderlichen Testfälle zu erstellen, um den Fehler zu erkennen.
Die einfache Lösung, bei der Sie möchten, dass sich eine Funktion darauf verlassen kann, dass sie
shared_ptr
weiterhin durchgehend ungleich Null ist, besteht darin, dass die Funktion ihr eigenes trueshared_ptr
zuweist, anstatt sich auf einen Verweis auf ein vorhandenes zu verlassenshared_ptr
.Der Nachteil ist, dass das Kopieren von a
shared_ptr
nicht kostenlos ist: Selbst "sperrenfreie" Implementierungen müssen eine ineinandergreifende Operation verwenden, um Threading-Garantien einzuhalten. Es kann also Situationen geben, in denen ein Programm durch Ändern von ashared_ptr
in a erheblich beschleunigt werden kannshared_ptr &
. Dies ist jedoch keine Änderung, die sicher an allen Programmen vorgenommen werden kann. Es ändert die logische Bedeutung des Programms.Beachten Sie, dass ein ähnlicher Fehler auftreten würde, wenn wir
std::string
anstelle vonstd::shared_ptr<std::string>
und anstelle von:Um die Nachricht zu löschen, sagten wir:
Dann wäre das Symptom das versehentliche Senden einer leeren Nachricht anstelle eines undefinierten Verhaltens. Die Kosten für eine zusätzliche Kopie einer sehr großen Zeichenfolge können viel höher sein als die Kosten für das Kopieren einer Zeichenfolge
shared_ptr
, sodass der Kompromiss unterschiedlich sein kann.quelle
Ich war mit der Antwort mit der höchsten Stimme nicht einverstanden, also suchte ich nach Expertenmeinungen und hier sind sie. Von http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything
Herb Sutter: "Wenn Sie shared_ptrs übergeben, sind Kopien teuer."
Scott Meyers: "Shared_ptr hat nichts Besonderes, wenn es darum geht, ob Sie es als Wert oder als Referenz übergeben. Verwenden Sie genau dieselbe Analyse, die Sie für jeden anderen benutzerdefinierten Typ verwenden. Die Leute scheinen diese Wahrnehmung zu haben, die shared_ptr irgendwie löst." Alle Verwaltungsprobleme, und das, weil es klein ist, ist es notwendigerweise kostengünstig, den Wert weiterzugeben. Es muss kopiert werden, und damit sind Kosten verbunden. Es ist teuer, den Wert weiterzugeben. Wenn ich also damit durchkommen kann Wenn ich die richtige Semantik in meinem Programm habe, werde ich es stattdessen als Verweis auf const oder als Referenz übergeben. "
Herb Sutter: "Übergeben Sie sie immer als Verweis auf const, und gelegentlich, vielleicht weil Sie wissen, was Sie aufgerufen haben, können Sie das ändern, von dem Sie eine Referenz erhalten haben. Vielleicht übergeben Sie sie dann als Wert ... wenn Sie sie als Parameter kopieren, oh Meine Güte, Sie müssen diesen Referenzzähler fast nie erhöhen, weil er sowieso am Leben gehalten wird, und Sie sollten ihn als Referenz weitergeben, also tun Sie das bitte. "
Update: Herb hat dies hier erweitert: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , obwohl die Moral der Geschichte lautet, dass Sie nicht passieren sollten shared_ptrs überhaupt "es sei denn, Sie möchten den Smart Pointer selbst verwenden oder manipulieren, z. B. um das Eigentum zu teilen oder zu übertragen."
quelle
shared_ptr
s gab, jedoch keine Einigung über das Problem der Übergabe von Wert und Referenz.const &
es nur sehr einfach ist , herauszufinden, ob sich das Ändern von Parametern auf die Semantik auswirkt einfache Programme.Ich würde von dieser Praxis abraten, es sei denn, Sie und die anderen Programmierer, mit denen Sie zusammenarbeiten, wissen wirklich , was Sie alle tun.
Erstens haben Sie keine Ahnung, wie sich die Schnittstelle zu Ihrer Klasse entwickeln könnte, und Sie möchten verhindern, dass andere Programmierer schlechte Dinge tun. Das Übergeben eines shared_ptr als Referenz sollte ein Programmierer nicht erwarten, da er nicht idiomatisch ist und es daher einfach ist, ihn falsch zu verwenden. Defensiv programmieren: Machen Sie die Schnittstelle schwer falsch zu bedienen. Das Übergeben als Referenz wird später nur zu Problemen führen.
Zweitens: Optimieren Sie erst, wenn Sie wissen, dass diese bestimmte Klasse ein Problem darstellt. Profil zuerst, und wenn Ihr Programm dann wirklich den Schub braucht, der durch Referenzübergabe gegeben wird, dann vielleicht. Schwitzen Sie andernfalls nicht die kleinen Dinge (dh die zusätzlichen N Anweisungen, die zum Übergeben des Werts erforderlich sind), sondern sorgen Sie sich um Design, Datenstrukturen, Algorithmen und langfristige Wartbarkeit.
quelle
Ja, eine Referenz ist dort in Ordnung. Sie beabsichtigen nicht, die Methode gemeinsam zu nutzen. es will nur damit arbeiten. Sie können auch für den ersten Fall eine Referenz verwenden, da Sie diese trotzdem kopieren. Aber für den ersten Fall übernimmt es das Eigentum. Es gibt diesen Trick, um es immer noch nur einmal zu kopieren:
Sie sollten auch kopieren, wenn Sie es zurückgeben (dh keine Referenz zurückgeben). Weil Ihre Klasse nicht weiß, was der Client damit macht (sie könnte einen Zeiger darauf speichern und dann passiert ein Urknall). Wenn sich später herausstellt, dass es sich um einen Engpass handelt (erstes Profil!), Können Sie trotzdem eine Referenz zurückgeben.
Bearbeiten : Wie andere betonen, gilt dies natürlich nur, wenn Sie Ihren Code kennen und wissen, dass Sie den übergebenen freigegebenen Zeiger nicht auf irgendeine Weise zurücksetzen. Im Zweifelsfall einfach den Wert übergeben.
quelle
Es ist sinnvoll,
shared_ptr
s vorbei zu gehenconst&
. Es wird wahrscheinlich keine Probleme verursachen (außer in dem unwahrscheinlichen Fall, dass der Verweisshared_ptr
während des Funktionsaufrufs gelöscht wird, wie von Earwicker beschrieben) und es wird wahrscheinlich schneller sein, wenn Sie viele davon weitergeben. Merken; Die Standardeinstellungboost::shared_ptr
ist threadsicher, daher enthält das Kopieren ein threadsicheres Inkrement.Versuchen Sie,
const&
nicht nur zu verwenden&
, da temporäre Objekte möglicherweise nicht als nicht konstante Referenz übergeben werden. (Auch wenn eine Spracherweiterung in MSVC dies trotzdem ermöglicht)quelle
Im zweiten Fall ist dies einfacher:
Sie können es als nennen
quelle
Ich würde eine "einfache" Referenz vermeiden, wenn die Funktion den Zeiger nicht explizit ändern könnte.
A
const &
kann eine sinnvolle Mikrooptimierung sein, wenn kleine Funktionen aufgerufen werden - z. B. um weitere Optimierungen zu ermöglichen, z. B. um einige Bedingungen aufzuheben. Außerdem ist das Inkrementieren / Dekrementieren - da es threadsicher ist - ein Synchronisationspunkt. Ich würde jedoch nicht erwarten, dass dies in den meisten Szenarien einen großen Unterschied macht.Im Allgemeinen sollten Sie den einfacheren Stil verwenden, es sei denn, Sie haben Grund, dies nicht zu tun. Verwenden Sie dann entweder das
const &
konsistente oder fügen Sie einen Kommentar hinzu, warum, wenn Sie es nur an wenigen Stellen verwenden.quelle
Ich würde empfehlen, einen gemeinsam genutzten Zeiger als const-Referenz zu übergeben - eine Semantik, dass die mit dem Zeiger übergebene Funktion NICHT den Zeiger besitzt, was für Entwickler eine saubere Redewendung ist.
Die einzige Gefahr besteht darin, dass in mehreren Thread-Programmen das Objekt, auf das der gemeinsam genutzte Zeiger zeigt, in einem anderen Thread zerstört wird. Es ist also sicher zu sagen, dass die Verwendung der const-Referenz des gemeinsam genutzten Zeigers in einem Single-Thread-Programm sicher ist.
Das Übergeben eines gemeinsam genutzten Zeigers als nicht konstante Referenz ist manchmal gefährlich. Der Grund dafür sind die Swap- und Reset-Funktionen, die die Funktion möglicherweise im Inneren aufruft, um das Objekt zu zerstören, das nach der Rückkehr der Funktion immer noch als gültig angesehen wird.
Ich denke, es geht nicht um vorzeitige Optimierung - es geht darum, unnötige Verschwendung von CPU-Zyklen zu vermeiden, wenn Sie klar sind, was Sie tun möchten, und die Codierungssprache von Ihren Mitentwicklern fest übernommen wurde.
Nur meine 2 Cent :-)
quelle
Es scheint, dass alle Vor- und Nachteile hier tatsächlich auf JEDEN Typ verallgemeinert werden können, der als Referenz übergeben wird, nicht nur shared_ptr. Meiner Meinung nach sollten Sie die Semantik der Übergabe von Referenz, Konstante und Wert kennen und richtig verwenden. Aber es ist absolut nichts an sich falsch, shared_ptr als Referenz zu übergeben, es sei denn, Sie denken, dass alle Referenzen schlecht sind ...
Um zum Beispiel zurückzukehren:
Woher weißt du, dass
sp.do_something()
das nicht durch einen baumelnden Zeiger explodiert?Die Wahrheit ist, dass, ob shared_ptr oder nicht, const oder nicht, dies passieren kann, wenn Sie einen Konstruktionsfehler haben, z. B. direkt oder indirekt das Eigentum
sp
zwischen Threads teilen, ein Objekt missbrauchen, das dies tutdelete this
, Sie ein zirkuläres Eigentum oder andere Eigentumsfehler haben.quelle
Eine Sache, die ich noch nicht erwähnt habe, ist, dass Sie beim Übergeben von gemeinsam genutzten Zeigern als Referenz die implizite Konvertierung verlieren, die Sie erhalten, wenn Sie einen abgeleiteten gemeinsam genutzten Klassenzeiger über einen Verweis auf einen gemeinsam genutzten Basisklassenzeiger übergeben möchten.
Dieser Code erzeugt beispielsweise einen Fehler, funktioniert jedoch, wenn Sie ihn
test()
so ändern , dass der gemeinsam genutzte Zeiger nicht als Referenz übergeben wird.quelle
Ich gehe davon aus, dass Sie mit vorzeitiger Optimierung vertraut sind sind und dies entweder für akademische Zwecke oder weil Sie bereits vorhandenen Code isoliert haben, der nicht ausreichend funktioniert.
Das Übergeben als Referenz ist in Ordnung
Das Übergeben einer konstanten Referenz ist besser und kann normalerweise verwendet werden, da es dem Objekt, auf das gezeigt wird, keine Konstanz aufzwingt.
Es besteht keine Gefahr, dass Sie den Zeiger aufgrund der Verwendung einer Referenz verlieren. Diese Referenz ist ein Beweis dafür, dass Sie eine Kopie des Smart Pointers früher im Stapel haben und nur ein Thread einen Aufrufstapel besitzt, sodass eine bereits vorhandene Kopie nicht verschwindet.
Die Verwendung von Referenzen ist aus den von Ihnen genannten Gründen häufig effizienter, jedoch nicht garantiert . Denken Sie daran, dass das Dereferenzieren eines Objekts auch Arbeit erfordern kann. Ihr ideales Referenzverwendungsszenario wäre, wenn Ihr Codierungsstil viele kleine Funktionen umfasst, bei denen der Zeiger vor der Verwendung von Funktion zu Funktion zu Funktion übergeben wird.
Sie sollten es immer vermeiden, Ihren Smart Pointer als Referenz zu speichern . Ihr
Class::take_copy_of_sp(&sp)
Beispiel zeigt die korrekte Verwendung dafür.quelle
Angenommen, es geht uns nicht um die Richtigkeit von Konstanten (oder mehr, Sie möchten, dass die Funktionen die übergebenen Daten ändern oder deren Besitz teilen können), ist das Übergeben eines boost :: shared_ptr nach Wert sicherer als das Übergeben als Referenz als Wir erlauben dem ursprünglichen boost :: shared_ptr, seine eigene Lebensdauer zu steuern. Betrachten Sie die Ergebnisse des folgenden Codes ...
Aus dem obigen Beispiel sehen wir , dass Passieren sharedA durch Bezugnahme erlaubt FooTakesReference die ursprünglichen Zeiger zurückgesetzt werden , die es der Verwendungszähler auf 0 reduziert, es die Daten zu zerstören. FooTakesValue kann den ursprünglichen Zeiger jedoch nicht zurücksetzen, wodurch sichergestellt wird, dass die Daten von sharedB weiterhin verwendet werden können. Wenn ein anderer Entwickler unweigerlich vorbeikommt und versucht, auf die fragile Existenz von sharedA zurückzugreifen , entsteht Chaos. Der glückliche SharedB- Entwickler geht jedoch früh nach Hause, da in seiner Welt alles in Ordnung ist.
Die Codesicherheit überwiegt in diesem Fall bei weitem die durch das Kopieren verursachte Geschwindigkeitsverbesserung. Gleichzeitig soll boost :: shared_ptr die Codesicherheit verbessern. Es wird viel einfacher sein, von einer Kopie zu einer Referenz zu wechseln, wenn etwas diese Art der Nischenoptimierung erfordert.
quelle
Sandy schrieb: "Es scheint, dass alle Vor- und Nachteile hier tatsächlich auf JEDEN Typ verallgemeinert werden können, der als Referenz übergeben wird, nicht nur shared_ptr."
Bis zu einem gewissen Grad wahr, aber der Sinn der Verwendung von shared_ptr besteht darin, Bedenken hinsichtlich der Objektlebensdauer zu beseitigen und den Compiler dies für Sie erledigen zu lassen. Wenn Sie einen gemeinsam genutzten Zeiger als Referenz übergeben und Clients Ihres referenzgezählten Objekts erlauben, nicht konstante Methoden aufzurufen, die die Objektdaten freigeben könnten, ist die Verwendung eines gemeinsam genutzten Zeigers fast sinnlos.
Ich habe in diesem vorherigen Satz "fast" geschrieben, weil Leistung ein Problem sein kann und in seltenen Fällen "gerechtfertigt" sein könnte, aber ich würde dieses Szenario auch selbst vermeiden und selbst nach allen möglichen anderen Optimierungslösungen suchen, um ernsthaft nachzuschauen beim Hinzufügen einer weiteren Ebene der Indirektion, faulen Bewertung usw ..
Code, der über den Autor hinaus existiert oder sogar das Gedächtnis des Autors veröffentlicht, der implizite Annahmen über das Verhalten, insbesondere das Verhalten über die Objektlebensdauer, erfordert, erfordert eine klare, präzise und lesbare Dokumentation, und dann lesen ihn viele Clients sowieso nicht! Einfachheit übertrifft fast immer die Effizienz, und es gibt fast immer andere Möglichkeiten, effizient zu sein. Wenn Sie wirklich Werte als Referenz übergeben müssen, um ein tiefes Kopieren Ihrer Objekte mit Referenzzählung (und des Operators equals) durch Kopierkonstruktoren zu vermeiden, sollten Sie möglicherweise überlegen, wie Sie die tief kopierten Daten zu Zeigern mit Referenzzählung machen können schnell kopiert. (Dies ist natürlich nur ein Entwurfsszenario, das möglicherweise nicht auf Ihre Situation zutrifft.)
quelle
Ich habe in einem Projekt gearbeitet, bei dem das Prinzip sehr stark darin bestand, intelligente Zeiger nach Wert zu übergeben. Als ich gebeten wurde, eine Leistungsanalyse durchzuführen, stellte ich fest, dass die Anwendung zum Inkrementieren und Dekrementieren der Referenzzähler der intelligenten Zeiger zwischen 4 und 6% der verwendeten Prozessorzeit verbringt.
Wenn Sie die intelligenten Zeiger nach Wert übergeben möchten, um Probleme in seltsamen Fällen zu vermeiden, wie von Daniel Earwicker beschrieben, stellen Sie sicher, dass Sie den Preis verstehen, den Sie dafür zahlen.
Wenn Sie sich für eine Referenz entscheiden, besteht der Hauptgrund für die Verwendung der Konstantenreferenz darin, implizites Upcasting zu ermöglichen, wenn Sie einen gemeinsam genutzten Zeiger an ein Objekt aus einer Klasse übergeben müssen, die die in der Schnittstelle verwendete Klasse erbt.
quelle
Zusätzlich zu dem, was litb gesagt hat, möchte ich darauf hinweisen, dass es wahrscheinlich darum geht, im zweiten Beispiel eine konstante Referenz zu verwenden, damit Sie sicher sind, dass Sie es nicht versehentlich ändern.
quelle
Wert übergeben:
als Referenz übergeben:
Zeiger übergeben:
quelle
Jedes Code-Stück muss einen Sinn haben. Wenn Sie überall in der Anwendung einen gemeinsam genutzten Zeiger als Wert übergeben , bedeutet dies "Ich bin mir nicht sicher, was anderswo vor sich geht, daher bevorzuge ich die Sicherheit ". Dies ist kein gutes Vertrauenszeichen für andere Programmierer, die den Code konsultieren könnten.
Selbst wenn eine Funktion eine konstante Referenz erhält und Sie "unsicher" sind, können Sie dennoch eine Kopie des gemeinsam genutzten Zeigers am Kopf der Funktion erstellen, um dem Zeiger eine starke Referenz hinzuzufügen. Dies könnte auch als Hinweis auf das Design angesehen werden ("der Zeiger könnte an anderer Stelle geändert werden").
Also ja, IMO, der Standard sollte " Pass by Const Reference " sein.
quelle