Ist es in Ordnung, den Wert des Standardarguments als const-Referenz zurückzugeben?

26

Ist es in Ordnung, den Wert des Standardarguments per const-Referenz wie in den folgenden Beispielen zurückzugeben:

https://coliru.stacked-crooked.com/a/ff76e060a007723b

#include <string>

const std::string& foo(const std::string& s = std::string(""))
{
    return s;
}

int main()
{
    const std::string& s1 = foo();
    std::string s2 = foo();

    const std::string& s3 = foo("s");
    std::string s4 = foo("s");
}
Gefrorenes Herz
quelle
3
Einfacher Test: Ersetzen Sie ihn std::stringdurch eine eigene Klasse, damit Sie den Bau und die Zerstörung verfolgen können.
user4581301
1
@ user4581301 Wenn die Reihenfolge korrekt ist, würde dies jedoch nicht beweisen, dass das Konstrukt in Ordnung ist.
Peter - Stellen Sie Monica
6
@ user4581301 "Es schien zu funktionieren, als ich es versuchte" ist das absolut Schlimmste an undefiniertem Verhalten
HerrJoebob
Es sei darauf hingewiesen, dass die Frage in ihrem Wortlaut irreführend ist. Sie geben den Wert eines Standardarguments nicht als const-Referenz zurück, sondern als const-Referenz zu einer const-Referenz (... zu einem Standardargument).
Damon
2
@HerrJoebob Stimmen Sie der Aussage zu 100% zu, aber nicht dem Kontext, in dem Sie sie verwenden. So wie ich sie lese, wird diese Frage in "Wann ist die Lebensdauer des Objekts abgelaufen?" herauszufinden, wann der Destruktor aufgerufen wird, ist ein guter Weg, dies zu tun. Für eine automatische Variable sollte der Destruktor pünktlich aufgerufen werden, da sonst große Probleme auftreten.
user4581301

Antworten:

18

In Ihrem Code baumeln beide s1und s3baumeln Referenzen. s2und s4sind ok.

Beim ersten Aufruf wird das std::stringaus dem Standardargument erstellte temporäre leere Objekt im Kontext des Ausdrucks erstellt, der den Aufruf enthält. Daher wird es am Ende der Definition von sterben s1, was s1baumeln lässt.

Beim zweiten Aufruf wird das temporäre std::stringObjekt zum Initialisieren verwendet s2und stirbt dann ab.

Im dritten Aufruf wird das String-Literal "s"verwendet, um ein temporäres std::stringObjekt zu erstellen , das ebenfalls am Ende der Definition von stirbt s3und s3baumelt.

Beim vierten Aufruf wird das temporäre std::stringObjekt mit dem Wert "s"zum Initialisieren verwendet s4und stirbt dann ab.

Siehe C ++ 17 [class.temporary] /6.1

Ein temporäres Objekt, das in einem Funktionsaufruf (8.2.2) an einen Referenzparameter gebunden ist, bleibt bestehen, bis der vollständige Ausdruck abgeschlossen ist, der den Aufruf enthält.

Brian
quelle
1
Der interessante Teil der Antwort ist die Behauptung, dass das Standardargument im Kontext des Aufrufers erstellt wird. Dies scheint durch Guillaumes Standardzitat gestützt zu werden.
Peter - Stellen Sie Monica
2
@ Peter-ReinstateMonica Siehe [expr.call] / 4, "... Die Initialisierung und Zerstörung jedes Parameters erfolgt im Kontext der aufrufenden Funktion. ..."
Brian
8

Es ist nicht sicher :

Im Allgemeinen kann die Lebensdauer eines Temporärs nicht durch "Weitergabe" weiter verlängert werden: Eine zweite Referenz, die aus dem Verweis initialisiert wurde, an den das Temporär gebunden war, hat keinen Einfluss auf seine Lebensdauer.

Vergessenheit
quelle
Denken Sie also, dass dies std::string s2 = foo();gültig ist (schließlich wird kein Verweis explizit weitergegeben)?
Peter - Stellen Sie Monica
1
@ Peter-ReinstateMonica, dass man sicher ist, wenn ein neues Objekt erstellt wird. Meine Antwort bezieht sich lediglich auf die Verlängerung des Lebens. Die beiden anderen Antworten deckten bereits alles ab. Ich würde es in meinem nicht wiederholen.
Vergessenheit
5

Es hängt davon ab, was Sie danach mit der Zeichenfolge machen.

Wenn Ihre Frage ist, ist mein Code korrekt?dann ist es ja.

Aus [dcl.fct.default] / 2

[ Beispiel : Die Erklärung

void point(int = 3, int = 4);

deklariert eine Funktion, die mit null, eins oder zwei Argumenten vom Typ int aufgerufen werden kann. Es kann auf eine der folgenden Arten aufgerufen werden:

point(1,2);  point(1);  point();

Die letzten beiden Anrufe sind äquivalent zu point(1,4)und point(3,4)verbunden. - Beispiel beenden ]

Ihr Code entspricht also effektiv:

const std::string& s1 = foo(std::string(""));
std::string s2 = foo(std::string(""));

Ihr gesamter Code ist korrekt, aber in keinem dieser Fälle gibt es eine Verlängerung der Referenzlebensdauer, da der Rückgabetyp eine Referenz ist.

Da Sie eine Funktion mit einer temporären Zeichenfolge aufrufen, verlängert die Lebensdauer der zurückgegebenen Zeichenfolge die Anweisung nicht.

const std::string& s1 = foo(std::string("")); // okay

s1; // not okay, s1 is dead. s1 is the temporary.

Ihr Beispiel mit s2ist in Ordnung, da Sie vor dem Ende des Satements aus dem Temporären kopieren (oder verschieben). s3hat das gleiche Problem als s1.

Guillaume Racicot
quelle