Wie kann man std :: unique_ptr weitergeben?

83

Ich habe meinen ersten Versuch, C ++ 11 zu verwenden unique_ptr. Ich ersetze einen polymorphen Rohzeiger in einem meiner Projekte, das einer Klasse gehört, aber ziemlich häufig herumgereicht wird.

Früher hatte ich Funktionen wie:

bool func(BaseClass* ptr, int other_arg) {
  bool val;
  // plain ordinary function that does something...
  return val;
}

Aber ich merkte schnell, dass ich nicht wechseln konnte zu:

bool func(std::unique_ptr<BaseClass> ptr, int other_arg);

Da der Aufrufer den Zeigerbesitz auf die Funktion übernehmen müsste, was ich nicht will. Was ist die beste Lösung für mein Problem?

Ich dachte daran, den Zeiger wie folgt zu übergeben:

bool func(const std::unique_ptr<BaseClass>& ptr, int other_arg);

Aber ich fühle mich sehr unwohl dabei, erstens, weil es nicht instinktiv erscheint, etwas zu übergeben, das bereits _ptrals Referenz eingegeben wurde , was eine Referenz einer Referenz wäre. Zweitens, weil die Funktionssignatur noch größer wird. Drittens, weil im generierten Code zwei aufeinanderfolgende Zeiger-Indirektionen erforderlich wären, um meine Variable zu erreichen.

lvella
quelle

Antworten:

96

Wenn die Funktion den Pointee verwenden soll, übergeben Sie einen Verweis darauf. Es gibt keinen Grund, die Funktion so zu verknüpfen, dass sie nur mit einem intelligenten Zeiger funktioniert:

bool func(BaseClass& base, int other_arg);

Und an der Anrufstelle verwenden operator*:

func(*some_unique_ptr, 42);

Wenn das baseArgument null sein darf, behalten Sie alternativ die Signatur bei und verwenden Sie die get()Elementfunktion:

bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);
R. Martinho Fernandes
quelle
4
Das ist schön, aber Cam, die Sie std::unique_ptrfür einen std::vector<std::unique_ptr>Streit loswerden ?
Ciro Santilli 10 冠状 病 六四 事件 10
31

Der Vorteil der Verwendung std::unique_ptr<T>(abgesehen davon, dass Sie nicht daran denken müssen, aufzurufen deleteoder delete[]explizit) ist, dass sie garantiert, dass ein Zeiger entweder nullptrauf eine gültige Instanz des (Basis-) Objekts verweist. Ich werde darauf zurückkommen , nachdem ich Ihre Frage beantworten, aber die erste Nachricht ist DO intelligente Zeiger verwenden , um die Lebensdauer von dynamisch zugewiesenen Objekten zu verwalten.

Ihr Problem ist nun, wie Sie dies mit Ihrem alten Code verwenden können .

Mein Vorschlag ist, dass Sie immer Verweise auf das Objekt übergeben sollten, wenn Sie das Eigentum nicht übertragen oder teilen möchten . Deklarieren Sie Ihre Funktion folgendermaßen (je constnach Bedarf mit oder ohne Qualifikationsmerkmale):

bool func(BaseClass& ref, int other_arg) { ... }

Dann wird der Anrufer, der ein hat std::shared_ptr<BaseClass> ptr, entweder den nullptrFall behandeln oder ihn bitten bool func(...), das Ergebnis zu berechnen:

if (ptr) {
  result = func(*ptr, some_int);
} else {
  /* the object was, for some reason, either not created or destroyed */
}

Dies bedeutet, dass jeder Aufrufer versprechen muss, dass die Referenz gültig ist und dass sie während der Ausführung des Funktionskörpers weiterhin gültig ist.


Hier ist der Grund, warum ich fest davon überzeugt bin, dass Sie keine Rohzeiger oder Verweise auf intelligente Zeiger übergeben sollten.

Ein Rohzeiger ist nur eine Speicheradresse. Kann eine von (mindestens) 4 Bedeutungen haben:

  1. Die Adresse eines Speicherblocks, in dem sich Ihr gewünschtes Objekt befindet. ( das Gute )
  2. Die Adresse 0x0, von der Sie sicher sein können, ist nicht dereferenzierbar und hat möglicherweise die Semantik "nichts" oder "kein Objekt". ( das schlechte )
  3. Die Adresse eines Speicherblocks, der sich außerhalb des adressierbaren Bereichs Ihres Prozesses befindet (eine Dereferenzierung führt hoffentlich zum Absturz Ihres Programms). ( der Hässliche )
  4. Die Adresse eines Speicherblocks, der dereferenziert werden kann, aber nicht das enthält, was Sie erwarten. Möglicherweise wurde der Zeiger versehentlich geändert und zeigt jetzt auf eine andere beschreibbare Adresse (einer völlig anderen Variablen in Ihrem Prozess). Das Schreiben in diesen Speicherort macht manchmal während der Ausführung viel Spaß, da sich das Betriebssystem nicht beschwert, solange Sie dort schreiben dürfen. ( Zoinks! )

Die korrekte Verwendung intelligenter Zeiger verringert die eher beängstigenden Fälle 3 und 4, die normalerweise beim Kompilieren nicht erkennbar sind und die Sie im Allgemeinen nur zur Laufzeit erleben, wenn Ihr Programm abstürzt oder unerwartete Dinge ausführt.

Das Übergeben von intelligenten Zeigern als Argumente hat zwei Nachteile: Sie können die const-ness des spitzen Objekts nicht ändern, ohne eine Kopie zu erstellen (was den Overhead erhöht shared_ptrund nicht möglich ist unique_ptr), und Sie haben immer noch die zweite ( nullptr) Bedeutung.

Ich habe den zweiten Fall aus gestalterischer Sicht als ( den schlechten ) markiert . Dies ist ein subtileres Argument für Verantwortung.

Stellen Sie sich vor, was es bedeutet, wenn eine Funktion a nullptrals Parameter empfängt . Zuerst muss entschieden werden, was damit zu tun ist: Verwenden Sie einen "magischen" Wert anstelle des fehlenden Objekts? Verhalten komplett ändern und etwas anderes berechnen (für das das Objekt nicht erforderlich ist)? Panik und eine Ausnahme werfen? Was passiert außerdem, wenn die Funktion 2 oder 3 oder noch mehr Argumente per Rohzeiger akzeptiert? Es muss jeden von ihnen überprüfen und sein Verhalten entsprechend anpassen. Dies fügt der Eingabevalidierung ohne wirklichen Grund eine völlig neue Ebene hinzu.

Der Anrufer sollte derjenige sein, der über genügend Kontextinformationen verfügt, um diese Entscheidungen zu treffen. Mit anderen Worten, je schlechter Sie wissen, desto weniger erschreckend ist das Schlechte . Die Funktion sollte andererseits nur das Versprechen des Anrufers erfüllen, dass der Speicher, auf den er zeigt, sicher ist, wie beabsichtigt zu arbeiten. (Referenzen sind immer noch Speicheradressen, stellen jedoch konzeptionell ein Gültigkeitsversprechen dar.)

Victor Savu
quelle
25

Ich stimme Martinho zu, aber ich denke, es ist wichtig, auf die Besitzersemantik eines Pass-by-Reference hinzuweisen. Ich denke, die richtige Lösung besteht darin, hier eine einfache Referenz zu verwenden:

bool func(BaseClass& base, int other_arg);

Die allgemein akzeptierte Bedeutung einer Referenzübergabe in C ++ ist so, als ob der Aufrufer der Funktion der Funktion mitteilt: "Hier können Sie dieses Objekt ausleihen, verwenden und ändern (wenn nicht const), aber nur für die Dauer des Funktionskörpers. " Dies steht in keiner Weise im Widerspruch zu den Eigentumsregeln von, unique_ptrda das Objekt lediglich für einen kurzen Zeitraum ausgeliehen wird und keine tatsächliche Eigentumsübertragung stattfindet (wenn Sie Ihr Auto an jemanden verleihen, unterschreiben Sie den Titel zu ihm rüber?).

Auch wenn es schlecht erscheint (in Bezug auf Design, Codierungspraktiken usw.), die Referenz (oder sogar den Rohzeiger) aus dem zu ziehen unique_ptr, liegt dies nicht daran, dass sie perfekt mit den von festgelegten Regeln für den Besitz übereinstimmt die unique_ptr. Und dann gibt es natürlich noch andere nette Vorteile, wie eine saubere Syntax, keine Beschränkung auf nur Objekte, die a gehören unique_ptr, und so weiter.

Mikael Persson
quelle
0

Persönlich vermeide ich es, eine Referenz von einem Zeiger / Smart Pointer zu ziehen. Denn was passiert, wenn der Zeiger ist nullptr? Wenn Sie die Signatur in Folgendes ändern:

bool func(BaseClass& base, int other_arg);

Möglicherweise müssen Sie Ihren Code vor Nullzeiger-Dereferenzen schützen:

if (the_unique_ptr)
  func(*the_unique_ptr, 10);

Wenn die Klasse der alleinige Eigentümer des Zeigers ist, erscheint die zweite Alternative von Martinho vernünftiger:

func(the_unique_ptr.get(), 10);

Alternativ können Sie verwenden std::shared_ptr. Wenn jedoch nur eine einzige Stelle verantwortlich ist delete, std::shared_ptrzahlt sich der Overhead nicht aus.

Guilherme Ferreira
quelle
Ist Ihnen bewusst, dass in den meisten Fällen std::unique_ptrkein Overhead anfällt ?
lvella
1
Ja, ich bin mir dessen bewusst. Deshalb habe ich ihm von dem std::shared_ptrOverhead erzählt .
Guilherme Ferreira