Sollten wir ein shared_ptr als Referenz oder als Wert übergeben?

269

Wenn eine Funktion eine shared_ptr(von Boost oder C ++ 11 STL) benötigt, übergeben Sie sie:

  • durch konstante Referenz: void foo(const shared_ptr<T>& p)

  • oder nach Wert : void foo(shared_ptr<T> p)?

Ich würde die erste Methode bevorzugen, weil ich vermute, dass sie schneller sein würde. Aber lohnt sich das wirklich oder gibt es zusätzliche Probleme?

Könnten Sie bitte die Gründe für Ihre Wahl angeben oder, falls dies der Fall ist, warum Sie denken, dass dies keine Rolle spielt.

Danvil
quelle
14
Das Problem ist, dass diese nicht gleichwertig sind. Die Referenzversion schreit: "Ich werde einige aliasen shared_ptrund ich kann es ändern, wenn ich will.", Während die Wertversion sagt: "Ich werde Ihre kopieren shared_ptr, also werden Sie es nie erfahren, solange ich sie ändern kann." ) Ein const-reference-Parameter ist die eigentliche Lösung, die besagt: "Ich werde einige shared_ptr
aliasen
2
Hey, ich würde mich für die Meinung eurer Jungs über die Rückkehr eines shared_ptrKlassenmitglieds interessieren . Machst du es mit const-refs?
Johannes Schaub - litb
Die dritte Möglichkeit ist die Verwendung von std :: move () mit C ++ 0x, dies tauscht beide shared_ptr
Tomaka17
@Johannes: Ich würde es als const-Referenz zurückgeben, nur um jegliches Kopieren / Nachzählen zu vermeiden. Andererseits gebe ich alle Mitglieder per const-Referenz zurück, es sei denn, sie sind primitiv.
GManNickG
Mögliches Duplikat von C ++ - Zeiger-Passing-Frage
Kennytm

Antworten:

228

Diese Frage wurde von Scott, Andrei und Herb während der Ask Us Anything- Sitzung bei C ++ and Beyond 2011 diskutiert und beantwortet . Achten Sie ab 4:34 auf shared_ptrLeistung und Korrektheit .

Kurz gesagt, es gibt keinen Grund, den Wert zu übergeben, es sei denn, das Ziel besteht darin, das Eigentum an einem Objekt zu teilen (z. B. zwischen verschiedenen Datenstrukturen oder zwischen verschiedenen Threads).

Es sei denn, Sie können es verschieben, wie von Scott Meyers im oben verlinkten Diskussionsvideo erläutert, aber dies hängt mit der tatsächlichen Version von C ++ zusammen, die Sie verwenden können.

Ein wichtiges Update dieser Diskussion wurde während des interaktiven Panels der GoingNative 2012- Konferenz veröffentlicht : Fragen Sie uns alles! Das ist sehenswert, besonders ab 22:50 Uhr .

Mloskot
quelle
5
Wie hier gezeigt, ist es jedoch billiger, den Wert zu übergeben: stackoverflow.com/a/12002668/128384 sollte dies nicht ebenfalls berücksichtigen (zumindest für Konstruktorargumente usw., bei denen ein shared_ptr Mitglied von wird die Klasse)?
stijn
2
@stijn Ja und nein. Die Fragen und Antworten, auf die Sie hinweisen, sind unvollständig, es sei denn, sie klären die Version des C ++ - Standards, auf den sie sich beziehen. Es ist sehr einfach, allgemeine nie / immer Regeln zu verbreiten, die einfach irreführend sind. Es sei denn, die Leser nehmen sich Zeit, um sich mit David Abrahams Artikeln und Referenzen vertraut zu machen, oder berücksichtigen das Veröffentlichungsdatum im Vergleich zum aktuellen C ++ - Standard. Daher sind beide Antworten, meine und die, auf die Sie hingewiesen haben, zum Zeitpunkt der Veröffentlichung korrekt.
Mloskot
1
"es sei denn, es gibt Multithreading " nein, MT ist in keiner Weise etwas Besonderes.
Neugieriger
3
Ich bin super spät zur Party, aber mein Grund, shared_ptr als Wert zu übergeben, ist, dass der Code dadurch kürzer und hübscher wird. Ernsthaft. Value*ist kurz und lesbar, aber es ist schlecht, also ist mein Code jetzt voll const shared_ptr<Value>&und es ist deutlich weniger lesbar und nur ... weniger aufgeräumt. Was früher war, void Function(Value* v1, Value* v2, Value* v3)ist jetzt void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3)und die Leute sind damit einverstanden?
Alex
7
@Alex Üblicherweise werden Aliase (typedefs) direkt nach der Klasse erstellt. Für Ihr Beispiel: class Value {...}; using ValuePtr = std::shared_ptr<Value>;Dann wird Ihre Funktion einfacher: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)und Sie erhalten maximale Leistung. Deshalb verwenden Sie C ++, nicht wahr? :)
4LegsDrivenCat
91

Hier ist Herb Sutters Einstellung

Richtlinie: Übergeben Sie keinen Smart Pointer als Funktionsparameter, es sei denn, Sie möchten den Smart Pointer selbst verwenden oder bearbeiten, z. B. um das Eigentum zu teilen oder zu übertragen.

Richtlinie: Drücken Sie aus, dass eine Funktion den Besitz eines Heap-Objekts mithilfe eines by-value-Parameters shared_ptr speichert und gemeinsam nutzt.

Richtlinie: Verwenden Sie nur einen nicht-const shared_ptr & -Parameter, um den shared_ptr zu ändern. Verwenden Sie eine const shared_ptr & nur dann als Parameter, wenn Sie nicht sicher sind, ob Sie eine Kopie erstellen und den Besitz freigeben möchten. Andernfalls verwenden Sie stattdessen das Widget * (oder, falls nicht nullbar, ein Widget &).

acel
quelle
3
Danke für den Link zu Sutter. Es ist ein ausgezeichneter Artikel. Ich bin mit ihm im Widget * nicht einverstanden und bevorzuge optionales <Widget &>, wenn C ++ 14 verfügbar ist. Widget * ist gegenüber altem Code zu mehrdeutig.
Gleichnamiger
3
+1 für das Einfügen von Widget * und Widget & als Möglichkeiten. Das Übergeben von Widget * oder Widget & ist wahrscheinlich die beste Option, wenn die Funktion das Zeigerobjekt selbst nicht untersucht / ändert. Die Schnittstelle ist allgemeiner, da kein bestimmter Zeigertyp erforderlich ist und das Leistungsproblem des Referenzzählers shared_ptr ausgewichen ist.
Tgnottingham
4
Ich denke, dies sollte heute aufgrund der zweiten Richtlinie die akzeptierte Antwort sein. Es macht die aktuell akzeptierte Antwort eindeutig ungültig, das heißt: Es gibt keinen Grund, den Wert zu übergeben.
mbrt
63

Persönlich würde ich eine constReferenz verwenden. Es ist nicht erforderlich, den Referenzzähler zu erhöhen, um ihn für einen Funktionsaufruf erneut zu verringern.

Evan Teran
quelle
1
Ich habe Ihre Antwort nicht abgelehnt, aber bevor dies eine Frage der Präferenz ist, gibt es Vor- und Nachteile für jede der beiden zu berücksichtigenden Möglichkeiten. Und es wäre gut, diese Vor- und Nachteile zu kennen und zu diskutieren. Danach kann jeder selbst eine Entscheidung treffen.
Danvil
@Danvil: Unter Berücksichtigung der Funktionsweise shared_ptrist der einzige mögliche Nachteil, wenn keine Referenz angegeben wird, ein geringfügiger Leistungsverlust. Hier gibt es zwei Ursachen. a) Die Zeiger-Aliasing-Funktion bedeutet, dass Zeiger Daten enthalten und ein Zähler (möglicherweise 2 für schwache Refs) kopiert wird, sodass das Kopieren der Datenrunde etwas teurer ist. b) Die Atomreferenzzählung ist etwas langsamer als der alte Inkrementierungs- / Dekrementierungscode, wird jedoch benötigt, um threadsicher zu sein. Darüber hinaus sind die beiden Methoden für die meisten Absichten und Zwecke gleich.
Evan Teran
37

Als constReferenz übergeben, ist es schneller. Wenn Sie es aufbewahren müssen, sagen wir in einem Behälter, die ref. Die Anzahl wird durch den Kopiervorgang automatisch erhöht.

Nikolai Fetissov
quelle
4
Downvote aufgrund seiner Meinung ohne Zahlen, um es zu sichern.
Kwesolowski
22

Ich lief unter dem Code, einmal mit fooder Einnahme shared_ptrvon const&und wieder mit fooder Einnahme shared_ptrvon Wert.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Mit VS2015, x86 Release Build, auf meinem Intel Core 2 Quad (2,4 GHz) Prozessor

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

Die Version nach Wert war eine Größenordnung langsamer.
Wenn Sie eine Funktion synchron vom aktuellen Thread aufrufen, bevorzugen Sie die const&Version.

tcb
quelle
1
Können Sie sagen, welche Compiler-, Plattform- und Optimierungseinstellungen Sie verwendet haben?
Carlton
Ich habe den Debug-Build von vs2015 verwendet und die Antwort aktualisiert, um den Release-Build jetzt zu verwenden.
TCB
1
Ich bin gespannt, ob Sie beim Aktivieren der Optimierung mit beiden die gleichen Ergebnisse
Elliot Woods,
2
Optimierung hilft nicht viel. Das Problem ist ein Sperrkonflikt bezüglich des Referenzzählers auf der Kopie.
Alex
1
Das ist nicht der Punkt. Eine solche foo()Funktion nicht einmal sollte einen gemeinsamen Zeiger in erster Linie akzeptieren , weil es dieses Objekt nicht verwendet hat: es sollte eine akzeptieren int&und zu tun p = ++x;, ruft foo(*p);aus main(). Eine Funktion akzeptiert ein Smart-Pointer-Objekt, wenn es etwas damit tun muss. In den meisten Fällen müssen Sie es ( std::move()) an einen anderen Ort verschieben, sodass ein By-Value-Parameter keine Kosten verursacht.
eepp
14

Seit C ++ 11 sollten Sie es öfter als gedacht als Wert über const & nehmen .

Wenn Sie std :: shared_ptr (anstelle des zugrunde liegenden Typs T) verwenden, tun Sie dies, weil Sie etwas damit tun möchten.

Wenn Sie es irgendwo kopieren möchten , ist es sinnvoller, es per Kopie zu nehmen und std :: intern zu verschieben, als es durch const & zu nehmen und später zu kopieren. Dies liegt daran, dass Sie dem Aufrufer die Möglichkeit geben, beim Aufrufen Ihrer Funktion wiederum std :: move_ptr zu verschieben, wodurch Sie sich eine Reihe von Inkrementierungs- und Dekrementierungsoperationen ersparen. Oder nicht. Das heißt, der Aufrufer der Funktion kann entscheiden, ob er nach dem Aufrufen der Funktion std :: shared_ptr benötigt oder nicht, und abhängig davon, ob er sich bewegt oder nicht. Dies ist nicht erreichbar, wenn Sie an const & vorbeikommen, und daher ist es dann vorzugsweise, es als Wert zu nehmen.

Natürlich, wenn der Aufrufer sein shared_ptr länger benötigt (kann es also nicht std :: move) und Sie keine einfache Kopie in der Funktion erstellen möchten (sagen Sie, Sie möchten einen schwachen Zeiger oder Sie möchten nur manchmal um es zu kopieren, abhängig von einer Bedingung), dann ist ein const & möglicherweise immer noch vorzuziehen.

Zum Beispiel sollten Sie tun

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

Über

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Denn in diesem Fall erstellen Sie immer intern eine Kopie

Plätzchen
quelle
1

Da ich die Zeitkosten für den Shared_copy-Kopiervorgang nicht kenne, bei denen das atomare Inkrementieren und Dekrementieren erfolgt, litt ich unter einem viel höheren CPU-Auslastungsproblem. Ich hätte nie gedacht, dass atomares Inkrementieren und Dekrementieren so viel Kosten verursachen könnte.

Nach meinem Testergebnis dauert das Inkrementieren und Dekrementieren von int32-Atomen zwei- oder 40-mal länger als das Inkrementieren und Dekrementieren von nichtatomaren Atomen. Ich habe es auf 3GHz Core i7 mit Windows 8.1 bekommen. Das erstere Ergebnis kommt heraus, wenn kein Konflikt auftritt, das letztere, wenn eine hohe Wahrscheinlichkeit eines Konflikts auftritt. Ich denke daran, dass atomare Operationen endlich hardwarebasierte Sperren sind. Schloss ist Schloss. Schlechte Leistung, wenn Konflikte auftreten.

In diesem Fall verwende ich immer byref (const shared_ptr &) als byval (shared_ptr).

Hyunjik Bae
quelle
0

Es gab kürzlich einen Blog-Beitrag: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Die Antwort darauf lautet also: Gehen Sie (fast) nie vorbei const shared_ptr<T>&.
Übergeben Sie stattdessen einfach die zugrunde liegende Klasse.

Grundsätzlich sind die einzigen vernünftigen Parametertypen:

  • shared_ptr<T> - Ändern und übernehmen Sie das Eigentum
  • shared_ptr<const T> - Nicht ändern, Eigentum übernehmen
  • T& - Ändern, kein Eigentum
  • const T& - Nicht ändern, kein Eigentum
  • T - Nicht ändern, kein Eigentum, billig zu kopieren

Wie @accel in https://stackoverflow.com/a/26197326/1930508 hervorhob, lautet der Rat von Herb Sutter:

Verwenden Sie eine const shared_ptr & nur dann als Parameter, wenn Sie nicht sicher sind, ob Sie eine Kopie erstellen und den Besitz freigeben möchten

Aber in wie vielen Fällen sind Sie sich nicht sicher? Das ist also eine seltene Situation

Flammenfeuer
quelle
0

Es ist bekannt, dass das Übergeben von shared_ptr als Wert Kosten verursacht und nach Möglichkeit vermieden werden sollte.

Die Kosten für die Übergabe von shared_ptr

Die meiste Zeit würde es reichen, shared_ptr als Referenz und noch besser als const-Referenz zu übergeben.

Die cpp-Kernrichtlinie enthält eine spezielle Regel für die Übergabe von shared_ptr

R.34: Nehmen Sie einen shared_ptr-Parameter, um auszudrücken, dass eine Funktion Teilbesitzer ist

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Ein Beispiel dafür, wie die Übergabe von shared_ptr als Wert wirklich erforderlich ist, ist die Übergabe eines gemeinsam genutzten Objekts an einen asynchronen Angerufenen, dh der Anrufer verlässt den Gültigkeitsbereich, bevor der Angerufene seinen Job beendet. Der Angerufene muss die Lebensdauer des gemeinsam genutzten Objekts "verlängern", indem er einen share_ptr nach Wert nimmt. In diesem Fall reicht es nicht, einen Verweis auf shared_ptr zu übergeben.

Gleiches gilt für die Übergabe eines gemeinsam genutzten Objekts an einen Arbeitsthread.

artm
quelle
-4

shared_ptr ist nicht groß genug, und sein Konstruktor \ destructor leistet nicht genug Arbeit, um genügend Overhead von der Kopie zu haben, um sich um die Referenzübergabe und die Leistung beim Übergeben der Kopie zu kümmern.

Steinmetall
quelle
15
Hast du es gemessen?
Neugieriger
2
@stonemetal: Was ist mit atomaren Anweisungen beim Erstellen eines neuen shared_ptr?
Quarra
Da es sich um einen Nicht-POD-Typ handelt, wird in den meisten ABIs sogar ein Zeiger übergeben, wenn er "nach Wert" übergeben wird. Es ist überhaupt nicht das eigentliche Kopieren von Bytes. Wie Sie in der asm-Ausgabe sehen können, werden für die Übergabe eines By- shared_ptr<int>Werts mehr als 100 x 86-Anweisungen benötigt (einschließlich teurer locked-Anweisungen, um die Ref-Anzahl atomar zu erhöhen / zu verringern). Das Übergeben einer konstanten Referenz ist dasselbe wie das Übergeben eines Zeigers auf irgendetwas (und in diesem Beispiel im Godbolt-Compiler-Explorer verwandelt die Tail-Call-Optimierung dies in ein einfaches jmp anstelle eines Aufrufs: godbolt.org/g/TazMBU ).
Peter Cordes
TL: DR: Dies ist C ++, wo Kopierkonstruktoren viel mehr Arbeit leisten können als nur das Kopieren der Bytes. Diese Antwort ist totaler Müll.
Peter Cordes
2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Als Beispiel für gemeinsam genutzte Zeiger, die nach Wert und Referenz übergeben werden, sieht er eine Laufzeitdifferenz von ca. 33%. Wenn Sie an leistungskritischem Code arbeiten, erzielen Sie mit nackten Zeigern eine größere Leistungssteigerung. Gehen Sie also sicher an const ref vorbei, wenn Sie sich daran erinnern, aber es ist keine große Sache, wenn Sie es nicht tun. Es ist viel wichtiger, shared_ptr nicht zu verwenden, wenn Sie es nicht benötigen.
Steinmetall