¿Un miembro de la clase de referencia constante prolonga la vida de un temporal?

171

¿Por qué esto:

#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;
}

Dar salida de:

La respuesta es:

En vez de:

La respuesta es: cuatro

Kyle
fuente
39
Y solo por más diversión, si hubieras escrito cout << "The answer is: " << Sandbox(string("four")).member << endl;, entonces estaría garantizado que funcionaría.
77
@RogerPate ¿Podría explicar por qué?
Paolo M
16
Para alguien que tiene curiosidad, el ejemplo que Roger Pate publicó funciona porque la cadena ("cuatro") es temporal y esa temporal se destruye al final de la expresión completa , por lo que en su ejemplo cuando SandBox::memberse lee, la cadena temporal todavía está viva .
PcAF
1
La pregunta es: dado que escribir tales clases es peligroso, ¿hay una advertencia del compilador contra el paso de temporarios a tales clases , o hay una pauta de diseño (en Stroustroup?) Que prohíbe escribir clases que almacenen referencias? Sería mejor una pauta de diseño para almacenar punteros en lugar de referencias.
Grim Fandango
@PcAF: ¿Podría explicar por qué el temporal string("four")se destruye al final de la expresión completa y no después de que Sandboxsalga el constructor? La respuesta de Potatoswatter dice que un enlace temporal a un miembro de referencia en un ctor-initializer del constructor (§12.6.2 [class.base.init]) persiste hasta que el constructor sale.
Taylor Nichols

Respuestas:

166

Solo las const referencias locales prolongan la vida útil.

El estándar especifica dicho comportamiento en §8.5.3 / 5, [dcl.init.ref], la sección sobre inicializadores de declaraciones de referencia. La referencia en su ejemplo está vinculada al argumento del constructor n, y deja de ser válida cuando el objeto nestá fuera de alcance.

La extensión de por vida no es transitiva a través de un argumento de función. §12.2 / 5 [clase.temporal]:

El segundo contexto es cuando una referencia está vinculada a una temporal. El temporal al que está vinculada la referencia o el temporal que es el objeto completo de un subobjeto del cual está vinculado el temporal persiste durante la vida útil de la referencia, excepto como se especifica a continuación. Un enlace temporal a un miembro de referencia en el ctor-initializer de un constructor (§12.6.2 [class.base.init]) persiste hasta que el constructor salga. Un enlace temporal a un parámetro de referencia en una llamada de función (§5.2.2 [llamada expr.]) Persiste hasta la finalización de la expresión completa que contiene la llamada.

Agua de patata
fuente
49
También debería ver GotW # 88 para una explicación más amigable para los humanos: hierbasutter.com/2008/01/01/…
Nathan Ernst
1
Creo que sería más claro si el estándar dijera "El segundo contexto es cuando una referencia está vinculada a un valor". En el código de OP, se podría decir que memberestá vinculado a un temporal, porque la inicialización membercon nmedios para unirse memberal mismo objeto nestá vinculado, y de hecho es un objeto temporal en este caso.
MM
2
@MM Hay casos en que los inicializadores lvalue o xvalue que contienen un prvalue extenderán el prvalue. Mi documento de propuesta P0066 revisa el estado de las cosas.
Potatoswatter
1
A partir de C ++ 11, las referencias de Rvalue también prolongan la vida de un temporal sin requerir un constcuantificador.
GetFree
3
@KeNVinFavo sí, usar un objeto muerto siempre es UB
Potatoswatter
30

Aquí está la forma más simple de explicar lo que sucedió:

En main () creaste una cadena y la pasaste al constructor. Esta instancia de cadena solo existía dentro del constructor. Dentro del constructor, asignó miembro para apuntar directamente a esta instancia. Cuando cuando el ámbito abandonó el constructor, la instancia de cadena se destruyó, y el miembro luego señaló un objeto de cadena que ya no existía. Hacer que Sandbox.member apunte a una referencia fuera de su alcance no mantendrá esas instancias externas dentro del alcance.

Si desea arreglar su programa para mostrar el comportamiento que desea, realice los siguientes cambios:

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

Ahora temp pasará fuera del alcance al final de main () en lugar de al final del constructor. Sin embargo, esta es una mala práctica. Su variable miembro nunca debe ser una referencia a una variable que existe fuera de la instancia. En la práctica, nunca se sabe cuándo esa variable estará fuera de alcance.

Lo que recomiendo es definir Sandbox.member como un const string member;Esto copiará los datos del parámetro temporal en la variable miembro en lugar de asignar la variable miembro como el parámetro temporal en sí.

Squirrelsama
fuente
Si hago esto: const string & temp = string("four"); Sandbox sandbox(temp); cout << sandbox.member << endl;¿seguirá funcionando?
Yves
@Thomas const string &temp = string("four");da el mismo resultado que const string temp("four"); , a menos que lo use decltype(temp)específicamente
MM
@ MM Muchas gracias ahora entiendo totalmente esta pregunta.
Yves
However, this is bad practice.- ¿por qué? Si tanto la temperatura como el objeto que lo contiene usan almacenamiento automático en el mismo ámbito, ¿no es 100% seguro? Y si no haces eso, ¿qué harías si la cadena es demasiado grande y demasiado costosa para copiar?
max
2
@max, porque la clase no impone lo pasado temporalmente para tener el alcance correcto. Significa que un día puede olvidarse de este requisito, pasar un valor temporal no válido y el compilador no le avisará.
Alex Che
5

Técnicamente hablando, este programa no requiere que realmente envíe nada a la salida estándar (que es una secuencia almacenada para empezar).

  • El cout << "The answer is: "bit se emitirá "The answer is: "en el búfer de stdout.

  • Luego, el << sandbox.memberbit proporcionará la referencia colgante operator << (ostream &, const std::string &), que invoca un comportamiento indefinido .

Debido a esto, no se garantiza que suceda nada. El programa puede funcionar aparentemente bien o puede fallar sin siquiera descargar stdout, lo que significa que el texto "La respuesta es:" no aparecería en su pantalla.

Tanz87
fuente
2
Cuando hay UB, el comportamiento de todo el programa no está definido, no solo comienza en un punto particular de la ejecución. Por lo tanto, no podemos decir con certeza que "The answer is: "se escribirá en cualquier lugar.
Toby Speight
0

Debido a que su cadena temporal quedó fuera de alcance una vez que el constructor de Sandbox regresó, y la pila ocupada por ella fue reclamada para otros fines.

En general, nunca debe retener referencias a largo plazo. Las referencias son buenas para argumentos o variables locales, nunca miembros de la clase.

Fyodor Soikin
fuente
77
"Nunca" es una palabra terriblemente fuerte.
Fred Larson
17
nunca miembros de la clase a menos que necesite mantener una referencia a un objeto. Hay casos en los que necesita mantener referencias a otros objetos, y no copias, para esos casos las referencias son una solución más clara que los punteros.
David Rodríguez - dribeas
0

te estás refiriendo a algo que ha desaparecido. Lo siguiente funcionará

#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
fuente