Gemeinsame Zeiger als Argumente übergeben

87

Wenn ich ein Objekt deklariere, das in einen gemeinsamen Zeiger eingeschlossen ist:

std::shared_ptr<myClass> myClassObject(new myClass());

dann wollte ich es als Argument an eine Methode übergeben:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

Erhöht das oben Gesagte einfach die Referenzanzahl von shared_pt und alles ist cool? Oder hinterlässt es einen baumelnden Zeiger?

Solltest du das noch tun?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Ich denke, dass der zweite Weg effizienter sein kann, da nur 1 Adresse kopiert werden muss (im Gegensatz zum gesamten Smart Pointer), aber der erste Weg scheint besser lesbar zu sein und ich erwarte nicht, die Leistungsgrenzen zu überschreiten. Ich möchte nur sicherstellen, dass nichts Gefährliches daran ist.

Danke dir.

Steve H.
quelle
14
const std::shared_ptr<myClass>& arg1
Kapitän Obvlious
3
Der zweite Weg ist unterbrochen, der erste ist idiomatisch, wenn Sie Ihre Funktion tatsächlich benötigen, um das Eigentum zu teilen. Aber muss man DoSomethingwirklich das Eigentum teilen? Es sieht so aus, als ob es stattdessen nur eine Referenz nehmen sollte ...
ildjarn
8
@SteveH: Das tut es nicht, aber warum sollte Ihre Funktion ihren Aufrufern eine spezielle Semantik des Objektbesitzes aufzwingen, wenn sie diese nicht wirklich benötigt? Ihre Funktion macht nichts, was nicht besser wäre als void DoSomething(myClass& arg1).
ildjarn
2
@SteveH: Der gesamte Zweck von intelligenten Zeigern besteht darin, ungewöhnliche Probleme mit dem Besitz von Objekten zu lösen. Wenn Sie diese nicht haben, sollten Sie überhaupt keine intelligenten Zeiger verwenden. ; -] shared_ptr<>Insbesondere müssen Sie den Wert übergeben, um das Eigentum tatsächlich zu teilen.
ildjarn
4
Unabhängig: Im Allgemeinen std::make_sharedist es nicht nur effizienter, sondern auch sicherer als der std::shared_ptrKonstruktor.
R. Martinho Fernandes

Antworten:

170

Ich möchte einen gemeinsamen Zeiger an eine Funktion übergeben. Kannst du mir damit helfen?

Klar, das kann ich dir helfen. Ich gehe davon aus, dass Sie ein gewisses Verständnis der Besitzersemantik in C ++ haben. Ist das wahr?

Ja, ich bin ziemlich zufrieden mit dem Thema.

Gut.

Ok, ich kann mir nur zwei Gründe vorstellen, um ein shared_ptrArgument anzunehmen:

  1. Die Funktion möchte das Eigentum an dem Objekt teilen.
  2. Die Funktion führt eine Operation aus, die speziell für shared_ptrs funktioniert .

Für welches interessieren Sie sich?

Ich suche nach einer allgemeinen Antwort, also interessiere ich mich eigentlich für beides. Ich bin allerdings gespannt, was Sie in Fall 2 meinen.

Beispiele für solche Funktionen sind std::static_pointer_castbenutzerdefinierte Komparatoren oder Prädikate. Wenn Sie beispielsweise alle eindeutigen shared_ptr aus einem Vektor suchen müssen, benötigen Sie ein solches Prädikat.

Ah, wenn die Funktion tatsächlich den Smart Pointer selbst manipulieren muss.

Genau.

In diesem Fall sollten wir meiner Meinung nach als Referenz dienen.

Ja. Und wenn sich der Zeiger dadurch nicht ändert, möchten Sie die const-Referenz übergeben. Sie müssen nicht kopieren, da Sie das Eigentum nicht teilen müssen. Das ist das andere Szenario.

OK habe es. Lassen Sie uns über das andere Szenario sprechen.

Der, an dem Sie das Eigentum teilen? OK. Wie teilen Sie das Eigentum mit shared_ptr?

Durch Kopieren.

Dann muss die Funktion eine Kopie von a shared_ptrerstellen, richtig?

Offensichtlich. Also übergebe ich es durch einen Verweis auf const und kopiere es in eine lokale Variable?

Nein, das ist eine Pessimisierung. Wenn es als Referenz übergeben wird, hat die Funktion keine andere Wahl, als die Kopie manuell zu erstellen. Wenn es als Wert übergeben wird, wählt der Compiler die beste Wahl zwischen einer Kopie und einer Verschiebung und führt sie automatisch aus. Übergeben Sie also den Wert.

Guter Punkt. Ich muss mich öfter an den Artikel " Willst du Geschwindigkeit? Pass by Value " erinnern .

Warten Sie, was ist, wenn die Funktion das beispielsweise shared_ptrin einer Mitgliedsvariablen speichert ? Wird das nicht eine redundante Kopie machen?

Die Funktion kann das shared_ptrArgument einfach in seinen Speicher verschieben. Das Verschieben von a shared_ptrist billig, da es keine Referenzanzahl ändert.

Ah, gute Idee.

Aber ich denke an ein drittes Szenario: Was ist, wenn Sie das nicht manipulieren shared_ptroder das Eigentum teilen wollen?

In diesem Fall shared_ptrist für die Funktion völlig irrelevant. Wenn Sie den Pointee manipulieren möchten, nehmen Sie einen Pointee und lassen Sie die Anrufer auswählen, welche Besitzersemantik sie möchten.

Und sollte ich den Pointee als Referenz oder als Wert nehmen?

Es gelten die üblichen Regeln. Intelligente Zeiger ändern nichts.

Übergeben Sie den Wert, wenn ich kopieren möchte, und übergeben Sie ihn als Referenz, wenn ich eine Kopie vermeiden möchte.

Richtig.

Hmm. Ich denke, Sie haben noch ein anderes Szenario vergessen. Was ist, wenn ich das Eigentum teilen möchte, aber nur abhängig von einer bestimmten Bedingung?

Ah, ein interessanter Randfall. Ich erwarte nicht, dass das oft passiert. In diesem Fall können Sie entweder den Wert übergeben und die Kopie ignorieren, wenn Sie sie nicht benötigen, oder als Referenz übergeben und die Kopie erstellen, wenn Sie sie benötigen.

Ich riskiere eine redundante Kopie in der ersten Option und verliere einen möglichen Zug in der zweiten. Kann ich den Kuchen nicht essen und ihn auch haben?

Wenn Sie sich in einer Situation befinden, in der dies wirklich wichtig ist, können Sie zwei Überladungen bereitstellen, eine mit einer Konstantenwertreferenz und eine mit einer Wertreferenz. Eine Kopie, die andere bewegt sich. Eine perfekte Weiterleitungsfunktionsvorlage ist eine weitere Option.

Ich denke, das deckt alle möglichen Szenarien ab. Vielen Dank.

R. Martinho Fernandes
quelle
2
@ Jon: Wofür? Hier geht es darum, ob ich A oder B machen soll . Ich denke, wir alle wissen, wie man Objekte nach Wert / Referenz übergibt, nein?
sbi
9
Weil Prosa wie "Bedeutet das, dass ich sie mit einem Verweis auf const weitergebe, um die Kopie zu erstellen? Nein, das ist eine Pessimisierung" für einen Anfänger verwirrend ist. Wenn Sie einen Verweis auf const übergeben, wird keine Kopie erstellt.
Jon
1
@Martinho Ich bin nicht einverstanden mit der Regel "Wenn Sie den Pointee manipulieren möchten, nehmen Sie einen Pointee". Wie in dieser Antwort angegeben, kann es gefährlich sein, ein Objekt nicht vorübergehend zu besitzen. Meiner Meinung nach sollte standardmäßig eine Kopie für den vorübergehenden Besitz übergeben werden, um die Gültigkeit des Objekts für die Dauer des Funktionsaufrufs zu gewährleisten.
Radman
@radman Kein Szenario in der Antwort, auf die Sie verlinken, wendet diese Regel an, daher ist sie völlig irrelevant. Bitte schreiben Sie mir eine SSCCE, in der Sie als Referenz von a shared_ptr<T> ptr;(dh void f(T&)mit aufgerufen f(*ptr)) übergeben werden und das Objekt den Aufruf nicht überlebt. Alternativ schreiben Sie eine, bei der Sie an Wert void f(T)und Fehler vorbeikommen .
R. Martinho Fernandes
@Martinho Schauen Sie sich meinen Beispielcode für das einfache, in sich geschlossene richtige Beispiel an. Das Beispiel ist für void f(T&)und beinhaltet Threads. Ich denke, mein Problem ist, dass der Ton Ihrer Antwort davon abhält, das Eigentum an shared_ptrs zu übergeben. Dies ist die sicherste, intuitivste und am wenigsten komplizierte Art, sie zu verwenden. Ich würde es extrem ablehnen, einem Anfänger jemals zu raten, einen Verweis auf Daten zu extrahieren, die einem shared_ptr <> gehören. Die Möglichkeiten für Missbrauch sind groß und der Nutzen ist minimal.
Radman
22

Ich denke, die Leute haben unnötig Angst davor, rohe Zeiger als Funktionsparameter zu verwenden. Wenn die Funktion den Zeiger nicht speichern oder seine Lebensdauer auf andere Weise beeinflussen soll, funktioniert ein Rohzeiger genauso gut und repräsentiert den kleinsten gemeinsamen Nenner. Überlegen Sie zum Beispiel, wie Sie a unique_ptran eine Funktion übergeben würden, die a shared_ptrals Parameter verwendet, entweder nach Wert oder nach const-Referenz?

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Ein Rohzeiger als Funktionsparameter hindert Sie nicht daran, intelligente Zeiger im aufrufenden Code zu verwenden, wo es wirklich darauf ankommt.

Mark Ransom
quelle
8
Wenn Sie einen Zeiger verwenden, warum nicht einfach eine Referenz verwenden? DoSomething(*a_smart_ptr)
Xeo
5
@Xeo, du hast recht - das wäre noch besser. Manchmal müssen Sie jedoch die Möglichkeit eines NULL-Zeigers berücksichtigen.
Mark Ransom
1
Wenn Sie die Konvention haben, Rohzeiger als Ausgabeparameter zu verwenden, ist es einfacher, im aufrufenden Code zu erkennen, dass sich eine Variable ändern kann, z . B.: Vergleichen fun(&x)mit fun(x). Im letzteren Beispiel xkönnte entweder als Wert oder als const ref übergeben werden. Wie oben erwähnt, können Sie auch bestehen, nullptrwenn Sie nicht an der Ausgabe interessiert sind ...
Andreas Magnusson
2
@AndreasMagnusson: Diese Konvention "Zeiger als Ausgabeparameter" kann ein falsches Sicherheitsgefühl vermitteln, wenn Zeiger behandelt werden, die von einer anderen Quelle übergeben wurden. Denn in diesem Fall macht der Aufruf Spaß (x) und * x wird geändert und die Quelle hat kein & x, um Sie zu warnen.
Zan Lynx
Was ist, wenn der Funktionsaufruf den Umfang des Aufrufers überlebt? Es besteht die Möglichkeit, dass der Destruktor des gemeinsam genutzten ptr aufgerufen wurde, und jetzt versucht die Funktion, auf gelöschten Speicher zuzugreifen, was zu UB
Sridhar Thiagarajan
4

Ja, die gesamte Idee eines shared_ptr <> besteht darin, dass mehrere Instanzen denselben Rohzeiger enthalten können und der zugrunde liegende Speicher nur freigegeben wird, wenn dort die letzte Instanz von shared_ptr <> zerstört wird.

Ich würde einen Zeiger auf einen shared_ptr <> vermeiden, da dies den Zweck zunichte macht, da Sie sich jetzt wieder mit raw_pointers beschäftigen.

R Samuel Klatchko
quelle
2

Das Übergeben von Werten in Ihrem ersten Beispiel ist sicher, aber es gibt eine bessere Redewendung. Wenn möglich an const-Referenz übergeben - ich würde ja sagen, auch wenn es sich um intelligente Zeiger handelt. Ihr zweites Beispiel ist nicht gerade kaputt, aber es ist sehr !???. Dumm, nichts zu erreichen und einen Teil des Punktes der intelligenten Zeiger zu besiegen, und Sie in einer fehlerhaften Welt voller Schmerzen zurückzulassen, wenn Sie versuchen, Dinge zu dereferenzieren und zu modifizieren.

Djechlin
quelle
3
Niemand befürwortet das Übergeben eines Zeigers, wenn Sie rohen Zeiger meinen. Die Übergabe als Referenz, wenn kein Eigentumsstreit besteht, ist sicherlich idiomatisch. Der Punkt ist shared_ptr<>insbesondere, dass Sie eine Kopie erstellen müssen , um das Eigentum tatsächlich zu teilen; Wenn Sie einen Verweis oder Zeiger auf a shared_ptr<>haben, teilen Sie nichts und unterliegen denselben lebenslangen Problemen wie ein normaler Verweis oder Zeiger.
ildjarn
2
@ildjarn: In diesem Fall ist es in Ordnung, eine konstante Referenz zu übergeben. Das Original shared_ptrdes Aufrufers überdauert garantiert den Funktionsaufruf, sodass die Funktion bei der Verwendung von sicher ist shared_ptr<> const &. Wenn der Zeiger für einen späteren Zeitpunkt gespeichert werden muss, muss er kopiert werden (zu diesem Zeitpunkt muss eine Kopie erstellt werden, anstatt eine Referenz zu enthalten). Die Kosten für das Kopieren müssen jedoch nicht anfallen, es sei denn, Sie müssen dies tun es. Ich würde in diesem Fall eine ständige Referenz geben ...
David Rodríguez - Dribeas
3
@David: "Das Übergeben einer konstanten Referenz ist in diesem Fall in Ordnung. " Nicht in einer vernünftigen API - warum sollte eine API einen intelligenten Zeigertyp vorschreiben , den sie nicht einmal nutzt, sondern nur eine normale konstante Referenz verwendet? Das ist fast so schlimm wie mangelnde Konstanz. Wenn Ihr Punkt ist, dass es technisch nichts schadet, dann stimme ich sicherlich zu, aber ich denke nicht, dass es das Richtige ist.
ildjarn
2
... wenn andererseits die Funktion die Lebensdauer des Objekts nicht verlängern muss, können Sie das einfach shared_ptrvon der Schnittstelle entfernen und einen einfachen ( const) Verweis auf das spitze Objekt übergeben.
David Rodríguez - Dribeas
3
@ David: Was ist, wenn Ihr API-Konsument zunächst nicht verwendet shared_ptr<>? Jetzt ist Ihre API wirklich nur ein Schmerz im Nacken, da sie den Aufrufer dazu zwingt, die Semantik der Objektlebensdauer sinnlos zu ändern, nur um sie zu verwenden. Ich sehe darin nichts, was es wert wäre, befürwortet zu werden, außer zu sagen: "Es tut technisch nichts weh." Ich sehe keine kontroversen Aussagen zu "Wenn Sie kein gemeinsames Eigentum benötigen, verwenden Sie es nicht shared_ptr<>".
ildjarn
0

In Ihrer Funktion DoSomething ändern Sie ein Datenelement einer Klasseninstanz. Sie ändern myClass also das verwaltete Objekt (Rohzeiger) und nicht das Objekt (shared_ptr). Dies bedeutet, dass am Rückgabepunkt dieser Funktion alle gemeinsam genutzten Zeiger auf den verwalteten Rohzeiger ihr Datenelement sehen: myClass::someFieldgeändert auf einen anderen Wert.

In diesem Fall übergeben Sie ein Objekt an eine Funktion mit der Garantie, dass Sie es nicht ändern (es handelt sich um shared_ptr, nicht um das eigene Objekt).

Die Redewendung, um dies auszudrücken, ist durch: ein const ref, wie so

void DoSomething(const std::shared_ptr<myClass>& arg)

Ebenso versichern Sie dem Benutzer Ihrer Funktion, dass Sie der Liste der Eigentümer des Rohzeigers keinen weiteren Eigentümer hinzufügen. Sie haben jedoch die Möglichkeit beibehalten, das zugrunde liegende Objekt zu ändern, auf das der Rohzeiger zeigt.

CAVEAT: Das heißt, wenn auf irgendeine Weise jemand anruft, shared_ptr::resetbevor Sie Ihre Funktion aufrufen, und in diesem Moment der letzte shared_ptr ist, der den raw_ptr besitzt, wird Ihr Objekt zerstört und Ihre Funktion manipuliert einen baumelnden Zeiger auf ein zerstörtes Objekt. SEHR GEFÄHRLICH!!!

Bio-Mann
quelle