Sollte der C ++ Standard spezifiziert haben, dass das Übergeben einer R-Wert-Referenz eines Objekts an eine Funktion gleichbedeutend ist, um es zu zerstören?

Es wurde eine gewisse Unterstützung für die Semantik “destruktiver Verschiebungen” gefunden. Aber zuerst sollten Sie erkennen, dass dies aus einigen Gründen nicht so einfach ist, wie Sie denken:

  1. Destruktoren haben spezielle “Berechtigungen” in der C ++ – Sprache, die anderen Funktionen fehlen. Ein Destruktor-Aufruf beendet die Lebensdauer eines Objekts. Wenn das Verschieben von einem Objekt seine Lebensdauer beendet, wird das Objekt zwischen dem Verschieben und dem Ende seines Gültigkeitsbereichs in einem Zustand “leer als leer” belassen, in dem der Versuch, darauf zuzugreifen, undefiniertes Verhalten verursachen würde. Der Compiler kann diesen Fehler nicht abfangen, da nur zur Laufzeit bekannt ist, ob eine Verschiebung erfolgt.
  2. Der Destruktoraufruf müsste in der aufrufenden Funktion unterdrückt werden, aber die aufrufende Funktion kann nicht wissen, ob tatsächlich eine logische Verschiebung stattgefunden hat, da eine Funktion, die einen rWert-Referenzparameter verwendet, einfach auswählen kann, sich nicht von diesem zu verschieben, insbesondere wenn sie das anbieten möchte starke ausnahmegarantie.
  3. Wenn die Funktion allein durch das Binden eines Objekts an einen r-Wert-Referenzparameter für die Zerstörung des Objekts “verantwortlich” gemacht würde, könnte dies dazu führen, dass schwer zu findende Fehler auftreten, wenn die Funktion dies nicht tut. Der Compiler kann dies nicht abfangen.

Es sollte auch darauf hingewiesen werden, dass, wenn der Aufruf einer Funktion, die einen rvalue-Referenzparameter verwendet, den Destruktoraufruf im aufrufenden Bereich bedingungslos unterdrückt, der aufrufende Bereich immer noch ein verborgenes Flag benötigt, um zu verfolgen, ob ein solcher Aufruf stattgefunden hat, damit er entscheiden kann ob der Konstruktor ausgeführt werden soll. Dies würde also in den einfachsten Fällen, in denen ein solches Flag optimiert werden könnte, keine Anweisungen speichern. Oft kann der Destruktoraufruf aber ohnehin schon optimiert werden, wie im Fall von std::unique_ptr . Wenn Sie sich bedingungslos von einem lokalen std::unique_ptr entfernen, kann der Compiler den Code so optimieren, dass er am Ende des Blocks nicht prüft, ob der Zeiger null ist.

In Bezug auf die Vereinheitlichung destruktiver Maßnahmen gab es einige Aktivitäten. Ich zitiere aus N1377, das weitere Komplikationen enthält:

Destruktive Bewegungssemantik

Unter C ++ – Programmierern besteht ein erheblicher Wunsch nach einer so genannten destruktiven Bewegungssemantik. Dies ähnelt dem oben beschriebenen, aber das Quellobjekt wird nicht in einem gültigen konstruierten Zustand, sondern in einem zerstörten Zustand belassen. Der größte Vorteil eines destruktiven Verschiebungskonstruktors besteht darin, dass eine solche Operation für eine Klasse programmiert werden kann, die keinen gültigen ressourcenlosen Status hat. Beispielsweise könnte die einfache Zeichenfolgenklasse, die immer mindestens einen Zeichenpuffer enthält, einen destruktiven Verschiebungskonstruktor haben. Man überträgt einfach den Zeiger auf den Datenpuffer zum neuen Objekt und deklariert die Quelle als zerstört. Dies hat einen ersten Reiz sowohl in der Einfachheit als auch in der Effizienz. Die Einfachheit ist jedoch nur von kurzer Dauer.

Beim Umgang mit Klassenhierarchien wird die destruktive Verschiebungssemantik problematisch. Wenn Sie zuerst die Basis verschieben, besteht die Quelle aus einem konstruierten abgeleiteten Teil und einem zerstörten Basisteil. Wenn Sie das abgeleitete Teil zuerst verschieben, verfügt das Ziel über ein konstruiertes abgeleitetes Teil und ein noch nicht konstruiertes Basisteil. Keine der beiden Optionen scheint praktikabel zu sein. Mehrere Lösungen für dieses Dilemma wurden untersucht.
Eine mögliche Lösung besteht darin, einen lahmen Entenzustand für ein Objekt in der Mitte des Zuges zu definieren:

Ein Lame-Duck-Objekt ist ein Objekt, auf das Sie Datenelemente verweisen können, jedoch keine Basisobjekte oder Elementfunktionen, unabhängig davon, ob sie abgeleitet wurden oder nicht. Sobald ein Mitglied eines Objekts (Basis- oder Direktmitglied) bewegt wird, ist dieses Objekt eine lahme Ente. Ein Verstoß gegen die Zugriffsregeln eines Lame-Duck-Objekts führt zu undefiniertem Verhalten (keine Diagnose erforderlich).

Die Komplikationen werden erheblich. Das Missbrauchspotenzial ist sehr real, und der Compiler kann sich kaum vor dem Missbrauch schützen. Die Kosten sehen einfach zu hoch aus.

Eine andere Lösung besteht darin, nur zu deklarieren, dass ein Objekt keinen Konstruktor für zerstörende Verschiebungen haben kann, es sei denn, alle Basisklassen und Member verfügen über einen Konstruktor für zerstörungsfreie Verschiebungen. Somit kann der Konstruktor für destruktive Bewegungen die Basen und Elemente zerstörungsfrei bewegen, bevor er sich selbst destruktiv bewegt. Dies löst das Hierarchieproblem, zumindest für eine eingeschränkte Menge von Klassen. Es entstehen aber noch mehr Gefahren. Die Verwendung eines destruktiven Verschiebungskonstrukts muss mit der gleichen Sorgfalt erfolgen wie die Verwendung eines expliziten Destruktoraufrufs. Man kann weder ein Auto-Objekt noch ein statisches Objekt zerstörerisch bewegen. Nur Objekte mit dynamischer Speicherdauer können zerstörerisch verschoben werden. Und die Syntax für diese Semantik würde wahrscheinlich mehr Sprachunterstützung erfordern, vielleicht einen Destruktor mit einem ungültigen Argument.

Am Ende gaben wir dies einfach als zu viel Schmerz für nicht genug Gewinn auf. Der derzeitige Vorschlag verbietet jedoch keine destruktive Bewegungssemantik für die Zukunft. Dies könnte zusätzlich zu der in diesem Vorschlag beschriebenen zerstörungsfreien Bewegungssemantik erfolgen, falls jemand diese Fackel tragen möchte.

Möglicherweise ist auch die Lösung N4158 nur für Bibliotheken von Interesse.

Da dies zu schwierig war (siehe die ausgezeichnete Antwort von Brian Bi), besagt der Standard ausdrücklich, dass ein l-Wert, der als r-Wert-Referenz übergeben wurde, nach seiner Verwendung immer noch zwei Operationen unterstützen muss: Zerstörung und Zuweisung. Ich gehe davon aus, dass dies entweder einen Zuweisungsoperator oder eine Mitgliedsfunktion wie die assign () von std :: basic_string bedeutet (z. B. std :: string, was std :: basic_string ist). Beachten Sie, dass assign ein direkter Vorläufer des emplace-Konzepts ist, das implementiert wurde, bevor eine perfekte Weiterleitung vorhanden war. Daher wird es für jede Kombination von String-Konstruktor-Argumenten überladen, aus denen ein std :: basic_string neu erstellt wird.

Nein, weil es nicht gleichwertig ist. Was passiert, ist, dass die Funktion möglicherweise den Inhalt des Objekts stiehlt, das Sie an sie übergeben, das Objekt jedoch in einem gültigen Zustand belassen muss (beispielsweise leer) und der Destruktor aufgerufen wird (wenn das Objekt den Gültigkeitsbereich verlässt).