webentwicklung-frage-antwort-db.com.de

Exakter Moment der "Rückkehr" in einer C ++ - Funktion

Es scheint eine dumme Frage zu sein, aber es ist genau der Moment, an dem return xxx; wird in einer eindeutig definierten Funktion "ausgeführt"?

Sehen Sie sich bitte das folgende Beispiel an, um zu sehen, was ich meine ( hier live ):

#include <iostream>
#include <string>
#include <utility>

//changes the value of the underlying buffer
//when destructed
class Writer{
public:
    std::string &s;
    Writer(std::string &s_):s(s_){}
    ~Writer(){
        s+="B";
    }
};

std::string make_string_ok(){
    std::string res("A");
    Writer w(res);
    return res;
}


int main() {
    std::cout<<make_string_ok()<<std::endl;
} 

Was ich naiv erwarten würde, während make_string_ok wird genannt:

  1. Konstruktor für res wird aufgerufen (Wert von res ist "A")
  2. Konstruktor für w wird aufgerufen
  3. return res wird ausgeführt. Der aktuelle Wert von res sollte zurückgegeben werden (indem der aktuelle Wert von res kopiert wird), d. H. "A".
  4. Destruktor für w wird aufgerufen, der Wert von res wird "AB".
  5. Destruktor für res wird aufgerufen.

Also würde ich erwarten, "A"als Ergebnis, aber erhalte "AB" auf der Konsole gedruckt.

Auf der anderen Seite für eine etwas andere Version von make_string:

std::string make_string_fail(){
    std::pair<std::string, int> res{"A",0};
    Writer w(res.first);
    return res.first;
}

das Ergebnis ist wie erwartet - "A" ( siehe live ).

Gibt der Standard an, welcher Wert in den obigen Beispielen zurückgegeben werden soll, oder ist er nicht spezifiziert?

67
ead

Es ist RVO (+ Kopie als temporäres Ergebnis, das das Bild trübt), eine der Optimierungen, die das sichtbare Verhalten ändern dürfen:

10.9.5 Auswahl kopieren/verschieben (Schwerpunkte sind meine):

Wenn bestimmte Kriterien erfüllt sind, darf eine Implementierung die Konstruktion zum Kopieren/Verschieben eines Klassenobjekts weglassen, auch wenn der Konstruktor für die Operation zum Kopieren/Verschieben und/oder den Destruktor ausgewählt wurde für das Objekt haben Nebenwirkungen **. In solchen Fällen behandelt die Implementierung die Quelle und das Ziel des ausgelassenen Kopier-/Verschiebevorgangs einfach als zwei verschiedene Arten, auf dasselbe Objekt zu verweisen .

Diese Aufteilung von Kopier-/Verschiebevorgängen, die als Aufteilung von Kopien bezeichnet werden, ist unter den folgenden Umständen zulässig (die kombiniert werden können, um mehrere Kopien zu beseitigen):

  • in einer return-Anweisung in einer Funktion mit einem Klassenrückgabetyp, wenn der Ausdruck der Name eines nichtflüchtigen automatischen Objekts ist (mit Ausnahme eines Funktionsparameters oder einer Variablen, die vom Ausnahme-Deklaration eines Handlers) mit demselben Typ (cv-Qualifikation ignorierend) wie der Funktionsrückgabetyp , die Kopier-/Verschiebeoperation kann weggelassen werden, indem das automatische Objekt direkt in konstruiert wird das Rückgabeobjekt des Funktionsaufrufs
  • [...]

Abhängig davon, ob es angewendet wird, wird Ihre gesamte Prämisse falsch. Bei 1. wird der c'tor für res aufgerufen, aber das Objekt kann innerhalb von make_string_ok Oder außerhalb von leben.

Fall 1.

Die Aufzählungszeichen 2. und 3. kommen möglicherweise überhaupt nicht vor, aber dies ist ein Nebeneffekt. Das Ziel hatte Nebenwirkungen, die von Writer betroffen waren und außerhalb von make_string_ok Lagen. Dies war zufällig ein temporäres Objekt, das mithilfe von make_string_ok Im Kontext der Auswertung operator<<(ostream, std::string) erstellt wurde. Der Compiler hat einen temporären Wert erstellt und die Funktion ausgeführt. Dies ist wichtig, da temporär außerhalb von Writer gewohnt wird. Daher ist das Ziel für make_string_ok Nicht lokal, sondern auf operator<< Festgelegt.

Fall 2.

In der Zwischenzeit entspricht Ihr zweites Beispiel nicht dem Kriterium (noch den aus Gründen der Kürze weggelassenen), da die Typen unterschiedlich sind. So stirbt der Schriftsteller. Es würde sogar sterben, wenn es ein Teil von pair wäre. Hier wird also eine Kopie von res.first Als temporäres Objekt zurückgegeben, und dann wirkt sich der Befehl von Writer auf das Original res.first Aus, das im Begriff ist, selbst zu sterben.

Es scheint ziemlich offensichtlich, dass die Kopie erstellt wird, bevor Destruktoren aufgerufen werden, da das von copy zurückgegebene Objekt ebenfalls zerstört wird, sodass Sie es ansonsten nicht kopieren können.

Schließlich läuft es auf RVO hinaus, weil das d'tor von Writer entweder auf dem äußeren Objekt oder auf dem lokalen Objekt arbeitet, je nachdem, ob die Optimierung angewendet wird oder nicht.

Gibt der Standard an, welcher Wert in den obigen Beispielen zurückgegeben werden soll, oder ist er nicht spezifiziert?

Nein, die Optimierung ist optional, kann jedoch das beobachtbare Verhalten ändern. Es liegt im Ermessen des Compilers, ob er es anwendet oder nicht. Es ist eine Ausnahme von der "allgemeinen Als-ob" -Regel, die besagt, dass der Compiler jede Transformation durchführen darf, die das beobachtbare Verhalten nicht ändert.

Ein Argument dafür wurde in c ++ 17 obligatorisch, aber nicht Ihres. Der obligatorische Wert ist ein unbenannter temporärer Wert.

28
luk32

Aufgrund von Return Value Optimization (RVO) kann ein Destruktor für std::string res In make_string_ok Nicht aufgerufen werden. Das Objekt string kann auf der Seite des Aufrufers erstellt werden, und die Funktion initialisiert möglicherweise nur den Wert.

Der Code entspricht:

void make_string_ok(std::string& res){
    Writer w(res);
}

int main() {
    std::string res("A");
    make_string_ok(res);
}

Aus diesem Grund lautet der Rückgabewert "AB".

Im zweiten Beispiel wird RVO nicht angewendet, und der Wert wird genau beim Rückgabeaufruf in den zurückgegebenen Wert kopiert, und der Destruktor von Writer wird nach dem Kopieren auf res.first Ausgeführt.

6.6 Sprunganweisungen

Beim Verlassen eines Gültigkeitsbereichs (jedoch abgeschlossen) werden Destruktoren (12.4) für alle konstruierten Objekte mit automatischer Speicherdauer (3.7.2) (benannte Objekte oder temporäre Objekte), die in diesem Gültigkeitsbereich deklariert sind, in umgekehrter Reihenfolge ihrer Deklaration aufgerufen. Die Übertragung aus einer Schleife, aus einem Block oder zurück nach einer initialisierten Variablen mit automatischer Speicherdauer beinhaltet die Zerstörung von Variablen mit automatischer Speicherdauer, die zum Zeitpunkt der Übertragung von ...

...

6.6.3 Die Rückgabeerklärung

Die Kopierinitialisierung der zurückgegebenen Entität wird vor der Zerstörung von Temporären am Ende des durch den Operanden der return-Anweisung festgelegten vollständigen Ausdrucks sequenziert, der wiederum vor der Zerstörung lokaler Variablen (6.6) der Block, der die return-Anweisung einschließt.

...

12.8 Kopieren und Verschieben von Klassenobjekten

31 Wenn bestimmte Kriterien erfüllt sind, darf eine Implementierung die Konstruktion zum Kopieren/Verschieben eines Klassenobjekts auslassen, auch wenn der Konstruktor zum Kopieren/Verschieben und/oder der Destruktor für das Objekt Nebenwirkungen haben. In solchen Fällen behandelt die Implementierung die Quelle und das Ziel des ausgelassenen Kopier-/Verschiebevorgangs einfach als zwei verschiedene Arten, auf dasselbe Objekt zu verweisen, und die Zerstörung dieses Objekts tritt zu einem späteren Zeitpunkt auf, als die beiden Objekte gewesen wären ohne Optimierung zerstört. (123) Diese Aufhebung von Kopier-/Verschiebevorgängen, die als Aufhebung von Kopien bezeichnet wird, ist unter den folgenden Umständen zulässig (die kombiniert werden können, um mehrere Kopien zu beseitigen):

- in einer return-Anweisung in einer Funktion mit einem Klassenrückgabetyp, wenn der Ausdruck der Name eines nichtflüchtigen automatischen Objekts (mit Ausnahme einer Funktion oder eines catch-clause-Parameters) mit demselben cvunqualified-Typ wie der Funktionsrückgabetyp ist Der Kopier-/Verschiebevorgang kann weggelassen werden, indem das automatische Objekt direkt in den Rückgabewert der Funktion konstruiert wird

123) Da nur ein Objekt anstelle von zwei Objekten zerstört wird und ein Konstruktor zum Kopieren/Verschieben nicht ausgeführt wird, ist immer noch ein Objekt für jedes erstellte Objekt zerstört.

36
Shloim

In C++ gibt es ein Konzept namens elision.

Elision nimmt zwei scheinbar unterschiedliche Objekte und verbindet deren Identität und Lebensdauer.

Vor c ++ 17 konnte die Entscheidung erfolgen:

  1. Wenn Sie eine Nicht-Parameter-Variable Foo f; in einer Funktion haben, die Foo zurückgibt, und die return-Anweisung ein einfacher return f; ist.

  2. Wenn Sie ein anonymes Objekt haben, das verwendet wird, um so ziemlich jedes andere Objekt zu konstruieren.

In c ++ 17 werden alle (fast?) Fälle von # 2 durch die neuen prvalue-Regeln eliminiert; Eine Elision findet nicht mehr statt, da das, was zum Erstellen eines temporären Objekts verwendet wurde, dies nicht mehr tut. Stattdessen ist die Konstruktion des "Temporären" direkt an den permanenten Objektort gebunden.

Aufgrund der ABI, zu der ein Compiler kompiliert, ist eine Entscheidung nicht immer möglich. Zwei häufige Fälle, in denen dies möglich ist, sind die Rückgabewertoptimierung und die benannte Rückgabewertoptimierung.

RVO ist der Fall wie folgt:

Foo func() {
  return Foo(7);
}
Foo foo = func();

wobei wir einen Rückgabewert Foo(7) haben, der in den zurückgegebenen Wert und dann in die externe Variable foo zerlegt wird. Was 3 Objekte zu sein scheint (der Rückgabewert von foo(), der Wert in der return -Zeile und Foo foo), ist zur Laufzeit tatsächlich 1.

Vor c ++ 17 müssen die Konstruktoren copy/move vorhanden sein, und die Auswahl ist optional. in c ++ 17 aufgrund der neuen prvalue-regeln muss kein copy/move-konstruktor existieren und es gibt keine option für den compiler, hier muss 1 wert sein.

Der andere berühmte Fall heißt Rückgabewertoptimierung, NRVO. Dies ist der (1) oben genannte Ausscheidungsfall.

Foo func() {
  Foo local;
  return local;
}
Foo foo = func();

auch hier kann elision die Lebensdauer und Identität von Foo local, den Rückgabewert von func und Foo foo außerhalb von func zusammenführen.

Sogar c ++ 17 , die zweite Zusammenführung (zwischen dem Rückgabewert von func und Foo foo) ist nicht optional (und technisch gesehen ist der von func zurückgegebene Wert niemals ein Objekt, nur Ein Ausdruck, der dann an Foo foo) gebunden ist, der erste bleibt jedoch optional und erfordert einen Move- oder Copy-Konstruktor.

Elision ist eine Regel, die auftreten kann, selbst wenn das Eliminieren dieser Kopien, Zerstörungen und Konstruktionen beobachtbare Nebenwirkungen haben würde. Es ist keine "als-ob" -Optimierung. Stattdessen ist es eine subtile Veränderung von dem, was ein naiver Mensch unter C++ - Code versteht. Es eine "Optimierung" zu nennen, ist mehr als eine Fehlbezeichnung.

Die Tatsache, dass es optional ist und dass subtile Dinge es zerstören können, ist ein Problem.

Foo func(bool b) {
  Foo long_lived;
  long_lived.futz();
  if (b)
  {
    Foo short_lived;
    return short_lived;
  }
  return long_lived;
}

im obigen Fall ist es zwar für einen Compiler zulässig, sowohl Foo long_lived als auch Foo short_lived zu löschen, aber Implementierungsprobleme machen es im Grunde unmöglich, da die Lebensdauer beider Objekte nicht mit dem Rückgabewert von func zusammengeführt werden kann. Das Eliminieren von short_lived und long_lived ist nicht legal und ihre Lebensdauern überschneiden sich.

Sie können es immer noch unter as-if tun, aber nur, wenn Sie alle Nebenwirkungen von Destruktoren, Konstruktoren und .futz() untersuchen und verstehen können.