Czy można zwracać domyślną wartość argumentu przez stałe odniesienie?

26

Czy można zwracać domyślną wartość argumentu przez odwołanie do stałej, jak w poniższych przykładach:

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");
}
Zamrożone serce
źródło
3
Prosty test: zamień std::stringna własny, abyś mógł śledzić budowę i zniszczenie.
user4581301,
1
@ user4581301 Jeśli sekwencja jest poprawna, nie udowodni to, że konstrukcja jest w porządku.
Peter - przywróć Monikę
6
@ user4581301 „Wyglądało na to, że zadziałało, gdy go wypróbowałem” jest absolutnie najgorszą rzeczą w nieokreślonym zachowaniu
HerrJoebob
Należy zauważyć, że pytanie jest smakołykiem wprowadzającym w błąd w swoim brzmieniu. Nie zwracasz wartości domyślnego argumentu przez stałe odwołanie, ale zwracasz stałe odwołanie do stałego odwołania (... do domyślnego argumentu).
Damon,
2
@HerrJoebob Zgadzam się ze stwierdzeniem 100%, ale nie w kontekście, w którym go używasz. Sposób, w jaki go czytam, to pytanie rozwiązuje się w następujący sposób: „Kiedy kończy się żywotność obiektu?” ustalenie, kiedy wywoływany jest destruktor, to dobry sposób na zrobienie tego. W przypadku zmiennej automatycznej destruktor powinien zostać wywołany na czas lub masz duże problemy.
user4581301,

Odpowiedzi:

18

W kodzie, jak s1i s3są zwisające referencje. s2i s4są w porządku.

W pierwszym wywołaniu tymczasowy pusty std::stringobiekt utworzony z domyślnego argumentu zostanie utworzony w kontekście wyrażenia zawierającego wywołanie. Dlatego umrze pod koniec definicji s1, która pozostawia s1zwisające.

W drugim wywołaniu std::stringobiekt tymczasowy służy do inicjalizacji s2, a następnie umiera.

W trzecim wywołaniu literał łańcuchowy "s"jest używany do utworzenia std::stringobiektu tymczasowego, który również umiera na końcu definicji s3, pozostawiając s3zwisające.

W czwartym wywołaniu tymczasowy std::stringobiekt z wartością "s"jest używany do inicjalizacji, s4a następnie umiera.

Zobacz C ++ 17 [współczesna klasa] / 6.1

Obiekt tymczasowy powiązany z parametrem odniesienia w wywołaniu funkcji (8.2.2) trwa do momentu zakończenia pełnego wyrażenia zawierającego wywołanie.

Brian
źródło
1
Interesującą częścią odpowiedzi jest twierdzenie, że domyślny argument zostanie utworzony w kontekście osoby dzwoniącej. Wydaje się, że potwierdza to standardowy cytat Guillaume'a.
Peter - Przywróć Monikę
2
@ Peter-ReinstateMonica Patrz [expr.call] / 4, „... Inicjalizacja i zniszczenie każdego parametru następuje w kontekście funkcji wywołującej. ...”
Brian
8

To nie jest bezpieczne :

Zasadniczo czas życia tymczasowego nie może być dalej przedłużany przez „przekazanie go”: drugie odniesienie, zainicjowane z odniesienia, z którym związany był tymczasowy, nie wpływa na jego żywotność.

Zapomnienie
źródło
Czy uważasz, że std::string s2 = foo();jest to poprawne (w końcu żadne odniesienie nie jest jawnie przekazywane)?
Peter - Przywróć Monikę
1
@ Peter-ReinstateMonica, że ​​jest bezpieczny, ponieważ zostanie zbudowany nowy obiekt. Moja odpowiedź dotyczy jedynie ekspansji na całe życie. Pozostałe dwie odpowiedzi już obejmowały wszystko. Nie powtórzyłbym w moim.
Oblivion,
5

To zależy od tego, co zrobisz z łańcuchem później.

Jeśli masz pytanie, czy mój kod jest poprawny? więc tak to jest.

Z [dcl.fct.default] / 2

[ Przykład : deklaracja

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

deklaruje funkcję, którą można wywołać z zerowym, jednym lub dwoma argumentami typu int. Można go wywołać na jeden z poniższych sposobów:

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

Dwa ostatnie połączenia są odpowiednio równoważne point(1,4)i point(3,4). - koniec przykładu ]

Twój kod jest więc faktycznie równoważny z:

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

Cały kod jest poprawny, ale w żadnym z tych przypadków nie ma przedłużenia czasu życia referencji, ponieważ typ zwracany jest referencją.

Ponieważ wywołujesz funkcję tymczasową, czas życia zwracanego ciągu nie wydłuży instrukcji.

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

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

Twój przykład z s2jest w porządku, ponieważ kopiujesz (lub przenosisz) z tymczasowego przed końcem sytości. s3ma taki sam problem jak s1.

Racilla Guillaume
źródło