¿Por qué la vida útil temporal no se extiende hasta la vida útil del objeto circundante?

Sé que un temporal no puede vincularse a una referencia no constante, pero puede vincularse a una referencia constante. Es decir,

A & x = A(); //error const A & y = A(); //ok 

También sé que en el segundo caso (arriba), el tiempo de vida del temporal creado a partir de A() extiende hasta el tiempo de vida de la referencia constante (es decir, y ).

Pero mi pregunta es:

¿Puede la referencia constante, que está ligada a un temporal, estar más unida a otra referencia constante, extendiendo el tiempo de vida del temporal hasta la vida del segundo objeto?

Intenté esto y no funcionó. No entiendo exactamente esto. Escribí este código:

 struct A { A() { std::cout << " A()" << std::endl; } ~A() { std::cout << "~A()" << std::endl; } }; struct B { const A & a; B(const A & a) : a(a) { std::cout << " B()" << std::endl; } ~B() { std::cout << "~B()" << std::endl; } }; int main() { { A a; B b(a); } std::cout << "-----" << std::endl; { B b((A())); //extra braces are needed! } } 

Salida ( ideone ):

  A() B() ~B() ~A() ----- A() B() ~A() ~B() 

Diferencia en la salida? ¿Por qué el objeto temporal A() se destruye antes que el objeto b en el segundo caso? ¿El estándar (C ++ 03) habla de este comportamiento?

El estándar considera dos circunstancias bajo las cuales se extiende la vida de un temporal:

§12.2 / 4 Hay dos contextos en los que los temporales se destruyen en un punto diferente al final de la expresión de fullexpresión. El primer contexto es cuando aparece una expresión como un inicializador para un declarador que define un objeto. En ese contexto, el temporal que contiene el resultado de la expresión persistirá hasta que se complete la inicialización del objeto. […]

§12.2 / 5 El segundo contexto es cuando una referencia está vinculada a un temporal. […]

Ninguno de esos dos le permite extender la vida útil del temporal mediante un enlace posterior de la referencia a otra referencia constante. Pero ignora a los standarese y piensa en lo que está pasando:

Los temporarios se crean en la stack. Bueno, técnicamente, la convención de llamadas podría significar que un valor devuelto (temporal) que se ajusta a los registros puede que ni siquiera se cree en la stack, pero tenga paciencia conmigo. Cuando vincula una referencia constante a un temporal, el comstackdor crea semánticamente una variable con nombre oculto (es por eso que el constructor de copia debe ser accesible, incluso si no se llama) y vincula la referencia a esa variable. Si la copia se realiza o se elimina realmente es un detalle: lo que tenemos es una variable local sin nombre y una referencia a ella.

Si el estándar permitiera su caso de uso, significaría que la vida del temporal debería extenderse hasta la última referencia a esa variable. Ahora considere esta simple extensión de su ejemplo:

 B* f() { B * bp = new B(A()); return b; } void test() { B* p = f(); delete p; } 

Ahora el problema es que el temporal (llamémoslo _T ) está enlazado en f() , se comporta como una variable local allí. La referencia está vinculada dentro de *bp . Ahora la duración de ese objeto se extiende más allá de la función que creó el temporal, pero debido a que _T no se asignó dinámicamente, eso es imposible.

Puede probar y razonar el esfuerzo que se requeriría para extender la vida útil del temporal en este ejemplo, y la respuesta es que no se puede hacer sin algún tipo de GC.

No, la vida útil extendida no se amplía aún más pasando la referencia.

En el segundo caso, el temporal está vinculado al parámetro a, y se destruye al final de la vida útil del parámetro: el final del constructor.

La norma dice explícitamente:

Un límite temporal a un miembro de referencia en un ctor-initializer (12.6.2) del constructor persiste hasta que el constructor sale.

Su ejemplo no realiza una extensión de vida anidada

En el constructor

 B(const A & a_) : a(a_) { std::cout < < " B()" << std::endl; } 

El a_ aquí (renombrado para la exposición) no es temporal. Si una expresión es temporal es una propiedad sintáctica de la expresión, y una expresión de identificación nunca es temporal. Entonces, no hay extensión de por vida aquí.

Aquí hay un caso donde ocurriría una extensión de por vida:

 B() : a(A()) { std::cout < < " B()" << std::endl; } 

Sin embargo, dado que la referencia se inicializa en un inicializador de ctor, la vida útil solo se extiende hasta el final de la función. Por [class.temporary] p5 :

Un límite temporal a un miembro de referencia en un ctor-initializer (12.6.2) del constructor persiste hasta que el constructor sale.

En la llamada al constructor

 B b((A())); //extra braces are needed! 

Aquí, estamos vinculando una referencia a un temporal. [class.temporary] p5 dice:

Un límite temporal a un parámetro de referencia en una llamada a función (5.2.2) persiste hasta la finalización de la expresión completa que contiene la llamada.

Por lo tanto, el temporal A se destruye al final de la statement. Esto sucede antes de que la variable B se destruya al final del bloque, lo que explica su salida de registro.

Otros casos realizan una extensión de vida anidada

Inicialización de variable agregada

La inicialización agregada de una estructura con un miembro de referencia puede prolongarse de por vida:

 struct X { const A &a; }; X x = { A() }; 

En este caso, el temporal A está vinculado directamente a una referencia, por lo que el temporal se extiende por toda la vida a la duración de xa , que es la misma que la vida útil de x . (Advertencia: hasta hace poco, muy pocos comstackdores tenían este derecho).

Inicialización temporal global

En C ++ 11, puede usar la inicialización agregada para inicializar un temporal, y así obtener una extensión de por vida recursiva:

 struct A { A() { std::cout < < " A()" << std::endl; } ~A() { std::cout << "~A()" << std::endl; } }; struct B { const A &a; ~B() { std::cout << "~B()" << std::endl; } }; int main() { const B &b = B { A() }; std::cout << "-----" << std::endl; } 

Con el tronco Clang o g ++, esto produce el siguiente resultado:

  A() ----- ~B() ~A() 

Tenga en cuenta que tanto el temporal A como el temporal B tienen una duración vitalicia. Debido a que la construcción del temporal A termina primero, se destruye al final.

In std::initializer_list inicialización

C ++ 11 std::initializer_list realiza la vida-extensión como si vinculara una referencia a la matriz subyacente. Por lo tanto, podemos realizar una extensión de vida anidada usando std::initializer_list . Sin embargo, los compiler errors son comunes en esta área:

 struct C { std::initializer_list b; ~C() { std::cout < < "~C()" << std::endl; } }; int main() { const C &c = C{ { { A() }, { A() } } }; std::cout << "-----" << std::endl; } 

Produce con Clang trunk:

  A() A() ----- ~C() ~B() ~B() ~A() ~A() 

y con el tronco g ++:

  A() A() ~A() ~A() ----- ~C() ~B() ~B() 

Ambos son incorrectos; la salida correcta es:

  A() A() ----- ~C() ~B() ~A() ~B() ~A() 

§12.2 / 5 dice “El segundo contexto [cuando la vida de un temporal se extiende] es cuando una referencia está vinculada a un temporal.” Tomado literalmente, esto dice claramente que la duración de la vida debe extenderse en su caso; tu B::a está ciertamente ligado a un temporal. (Una referencia se une a un objeto, y no veo ningún otro objeto al que pueda vincularse). Sin embargo, esta es una redacción muy pobre; Estoy seguro de que lo que se quiere decir es “El segundo contexto es cuando se usa un temporal para inicializar una referencia”, y la duración extendida corresponde a la de la referencia iniciada con la expresión rvalue que crea el temporal, y no a la de ningún otro otras referencias que luego pueden estar vinculadas al objeto. Tal como está, la redacción requiere algo que simplemente no es implementable: considere:

 void f(A const& a) { static A const& localA = a; } 

llamado con:

 f(A()); 

¿Dónde debería poner el comstackdor A() (dado que generalmente no puede ver el código de f() , y no sabe acerca de la estática local al generar la llamada)?

Creo que, en realidad, vale la pena un DR.

Podría agregar que hay texto que sugiere fuertemente que mi interpretación del bash es correcta. Imagina que tienes un segundo constructor para B :

 B::B() : a(A()) {} 

En este caso, B::a se inicializaría directamente con un temporal; la duración de este temporal debe extenderse incluso por mi interpretación. Sin embargo, el estándar hace una excepción específica para este caso; tal temporal solo persiste hasta que el constructor sale (lo que nuevamente te deja con una referencia colgante). Esta excepción proporciona una indicación muy fuerte de que los autores del estándar no pretendían que las referencias de los miembros en una clase extendieran el tiempo de vida de los temporales a los que están destinados; nuevamente, la motivación es la implementabilidad. Imagina que en lugar de

 B b((A())); 

tu habías escrito:

 B* b = new B(A()); 

¿Dónde debe colocar el comstackdor el A() temporal para que su vida útil sea la del B asignado dinámicamente?

En su primera ejecución, los objetos se destruyen en el orden en que fueron empujados en la stack -> eso es push A, push B, pop B, pop A.

En la segunda ejecución, la vida de A termina con la construcción de b. Por lo tanto, crea A, crea B de A, termina la vida de A por lo que se destruye, y luego B se destruye. Tiene sentido…

No sé sobre estándares, pero puedo analizar algunos hechos que vi en algunas preguntas anteriores.

La primera salida es como está por razones obvias de que a y b están en el mismo ámbito. También se destruye a después de b porque se construyó antes que b .

Supongo que deberías estar más interesado en la segunda salida. Antes de comenzar, debemos tener en cuenta que después del tipo de creaciones de objetos (temporarios independientes):

 { A(); } 

dura solo hasta el siguiente ; y no por el bloque que lo rodea . Demo En su segundo caso, cuando lo haga,

 B b((A())); 

por lo tanto, A() se destruye tan pronto como termina la creación del objeto B() . Dado que, la referencia const puede vincularse a temporal, esto no dará error de comstackción. Sin embargo, seguramente dará como resultado un error lógico si intenta acceder a B::a , que ahora está obligado a estar fuera de la variable de ámbito.

§12.2 / 5 dice

Un límite temporal a un parámetro de referencia en una llamada a función (5.2.2) persiste hasta la finalización de la expresión completa que contiene la llamada.

Bastante cortado y secado, de verdad.