Czy członek klasy odniesienia const przedłuża żywotność tymczasowego?

171

Dlaczego to:

#include <string>
#include <iostream>
using namespace std;

class Sandbox
{
public:
    Sandbox(const string& n) : member(n) {}
    const string& member;
};

int main()
{
    Sandbox sandbox(string("four"));
    cout << "The answer is: " << sandbox.member << endl;
    return 0;
}

Podaj wynik:

Odpowiedź to:

Zamiast:

Odpowiedź brzmi: cztery

Kyle
źródło
39
I dla większej zabawy, gdybyś napisał cout << "The answer is: " << Sandbox(string("four")).member << endl;, to na pewno zadziała.
7
@RogerPate Czy możesz wyjaśnić dlaczego?
Paolo M
16
Dla kogoś ciekawego, na przykład Roger Pate napisał, że działa, ponieważ ciąg znaków („cztery”) jest tymczasowy, a ten tymczasowy jest niszczony pod koniec pełnego wyrażenia , więc w jego przykładzie, kiedy SandBox::memberjest czytany, tymczasowy ciąg jest nadal żywy .
PcAF
1
Pytanie brzmi: skoro pisanie takich klas jest niebezpieczne, czy istnieje kompilator ostrzegający przed przekazywaniem tymczasowych do takich klas , czy też istnieją wytyczne projektowe (w Stroustroup?), Które zabraniają pisania klas przechowujących referencje? Wytyczne projektowe dotyczące przechowywania wskaźników zamiast odniesień byłyby lepsze.
Grim Fandango,
@PcAF: Czy mógłbyś wyjaśnić, dlaczego tymczasowy string("four")jest niszczony na końcu pełnego wyrażenia, a nie po zamknięciu Sandboxkonstruktora? Odpowiedź Potatoswatter mówi, że tymczasowe powiązanie z elementem referencyjnym w inicjatorze ctor konstruktora (§12.6.2 [class.base.init]) utrzymuje się do momentu zakończenia konstruktora.
Taylor Nichols

Odpowiedzi:

166

Tylko lokalne const referencje przedłużają żywotność.

Norma określa takie zachowanie w §8.5.3 / 5, [dcl.init.ref], sekcja dotycząca inicjatorów deklaracji referencyjnych. Odwołanie w twoim przykładzie jest powiązane z argumentem konstruktora ni staje się nieprawidłowe, gdy obiekt njest powiązany z wyjściem poza zakres.

Rozszerzenie okresu istnienia nie jest przechodnie przez argument funkcji. §12.2 / 5 [klasa.czasowa]:

Drugi kontekst ma miejsce, gdy odniesienie jest powiązane z tymczasowym. Obiekt tymczasowy, z którym jest powiązane odniesienie lub tymczasowy, który jest kompletnym obiektem do podobiektu, z którym jest powiązany obiekt tymczasowy, utrzymuje się przez cały okres istnienia odwołania, z wyjątkiem przypadków określonych poniżej. Tymczasowe powiązanie z elementem referencyjnym w inicjatorze ctor konstruktora (§12.6.2 [class.base.init]) utrzymuje się do momentu zakończenia konstruktora. Tymczasowe powiązanie z parametrem referencyjnym w wywołaniu funkcji (§5.2.2 [wyrażenie wywołanie]) utrzymuje się aż do zakończenia pełnego wyrażenia zawierającego wywołanie.

Potatoswatter
źródło
49
Powinieneś także zobaczyć GotW # 88, aby uzyskać bardziej przyjazne dla człowieka wyjaśnienie: Herbsutter.com/2008/01/01/…
Nathan Ernst
1
Myślę, że byłoby jaśniej, gdyby norma brzmiała: „Drugi kontekst ma miejsce, gdy odniesienie jest związane z wartością pr”. W kodzie OP można by powiedzieć, że memberjest to związane z tymczasowym, ponieważ inicjalizacja memberza npomocą środków wiążących się memberz tym samym obiektem njest związana z tym samym obiektem i jest to w rzeczywistości obiekt tymczasowy w tym przypadku.
MM
2
@MM Istnieją przypadki, w których inicjatory lvalue lub xvalue zawierające prvalue rozszerzą prvalue. Moja propozycja P0066 zawiera przegląd stanu rzeczy.
Potatoswatter
1
Począwszy od C ++ 11, odwołania Rvalue również przedłużają żywotność obiektu tymczasowego bez konieczności stosowania constkwantyfikatora.
GetFree
3
@KeNVinFavo tak, używanie martwego obiektu jest zawsze UB
Potatoswatter
30

Oto najprostszy sposób, aby wyjaśnić, co się stało:

W main () utworzyłeś łańcuch i przekazałeś go do konstruktora. To wystąpienie ciągu istniało tylko w konstruktorze. Wewnątrz konstruktora przypisałeś elementowi członkowskiemu bezpośrednie wskazywanie tego wystąpienia. Gdy zakres opuścił konstruktora, wystąpienie ciągu zostało zniszczone, a element członkowski wskazał obiekt ciągu, który już nie istnieje. Jeśli Sandbox.member wskazuje odwołanie poza jego zakresem, nie będzie w zasięgu tych zewnętrznych wystąpień.

Jeśli chcesz naprawić program, aby wyświetlał pożądane zachowanie, wprowadź następujące zmiany:

int main()
{
    string temp = string("four");    
    Sandbox sandbox(temp);
    cout << sandbox.member << endl;
    return 0;
}

Teraz temp wyjdzie poza zakres na końcu main () zamiast na końcu konstruktora. Jest to jednak zła praktyka. Twoja zmienna składowa nigdy nie powinna być odniesieniem do zmiennej, która istnieje poza instancją. W praktyce nigdy nie wiadomo, kiedy ta zmienna wyjdzie poza zakres.

Polecam zdefiniowanie Sandbox.member jako parametru const string member;Spowoduje to skopiowanie danych tymczasowego parametru do zmiennej składowej zamiast przypisywania zmiennej składowej jako samego parametru tymczasowego.

Squirrelsama
źródło
Jeśli to zrobię: const string & temp = string("four"); Sandbox sandbox(temp); cout << sandbox.member << endl;czy to nadal będzie działać?
Yves
@Thomas const string &temp = string("four");daje ten sam wynik, co const string temp("four"); , chyba że używasz decltype(temp)specjalnie
MM
@MM Wielkie dzięki, teraz całkowicie rozumiem to pytanie.
Yves
However, this is bad practice.- czemu? Jeśli zarówno obiekt tymczasowy, jak i obiekt zawierający używają automatycznego przechowywania w tym samym zakresie, czy nie jest to w 100% bezpieczne? A jeśli tego nie zrobisz, co byś zrobił, jeśli ciąg jest zbyt duży i zbyt drogi do skopiowania?
maksymalnie
2
@max, ponieważ klasa nie narzuca przekazanego tymczasowo, aby miał poprawny zakres. Oznacza to, że pewnego dnia możesz zapomnieć o tym wymaganiu, przekazać nieprawidłową wartość tymczasową, a kompilator nie ostrzeże Cię.
Alex Che,
5

Technicznie rzecz biorąc, ten program nie jest wymagany do wysyłania czegokolwiek na standardowe wyjście (które na początku jest buforowanym strumieniem).

  • cout << "The answer is: "Nieco emituje "The answer is: "w buforze o standardowe wyjście.

  • Wtedy << sandbox.memberbit dostarczy wiszące odniesienie do operator << (ostream &, const std::string &), które wywoła niezdefiniowane zachowanie .

Z tego powodu nic nie jest gwarantowane. Program może działać pozornie dobrze lub może ulec awarii nawet bez opróżniania wyjścia standardowego - co oznacza, że ​​tekst „Odpowiedź brzmi:” nie pojawił się na ekranie.

Tanz87
źródło
2
Gdy istnieje UB, zachowanie całego programu jest niezdefiniowane - nie zaczyna się on tylko w określonym momencie wykonywania. Nie możemy więc powiedzieć na pewno, że "The answer is: "zostanie to zapisane w dowolnym miejscu.
Toby Speight
0

Ponieważ twój tymczasowy ciąg wyszedł poza zakres po zwróceniu konstruktora Sandbox, a zajmowany przez niego stos został odzyskany do innych celów.

Ogólnie rzecz biorąc, nigdy nie należy długo przechowywać referencji. Odwołania są dobre dla argumentów lub zmiennych lokalnych, nigdy dla członków klasy.

Fyodor Soikin
źródło
7
„Nigdy” to strasznie mocne słowo.
Fred Larson
17
nigdy nie klasy, chyba że musisz zachować odniesienie do obiektu. Są przypadki, w których musisz zachować odniesienia do innych obiektów, a nie ich kopie, ponieważ w takich przypadkach odniesienia są bardziej przejrzystym rozwiązaniem niż wskaźniki.
David Rodríguez - dribeas
0

odnosisz się do czegoś, co zniknęło. Poniższe będą działać

#include <string>
#include <iostream>

class Sandbox
{

public:
    const string member = " "; //default to whatever is the requirement
    Sandbox(const string& n) : member(n) {}//a copy is made

};

int main()
{
    Sandbox sandbox(string("four"));
    std::cout << "The answer is: " << sandbox.member << std::endl;
    return 0;
}
pcodex
źródło