c++ initialization language-lawyer temporary-objects order-of-execution

c++ - ¿Cuándo es exactamente destruido un inicializador temporal?



initialization language-lawyer (3)

Hoy construí este experimento, después de responder una pregunta.

struct A { bool &b; A(bool &b):b(b) { } ~A() { std::cout << b; } bool yield() { return true; } }; bool b = A(b).yield(); int main() { }

b tiene un valor false (resultante de la inicialización cero) antes de establecerlo en true mediante la inicialización dinámica. Si el temporal se destruye antes de que termine la inicialización de b , imprimiremos false , de lo contrario será true .

La especificación dice que el temporal se destruye al final de la expresión completa. Eso no parece estar ordenado con la inicialización de b . Así que me pregunto

  • ¿La especificación permite que una implementación imprima false y true en diferentes ejecuciones?

Clang imprime false para lo anterior, mientras que GCC imprime true . Esto me confunde. ¿Me perdí un texto de especificación que define el orden?


(Citando el estándar C ++ 03)

Primero hay §12.2 / 3:

Cuando una implementación introduce un objeto temporal de una clase que tiene un constructor no trivial (12.1), se asegurará de que se llame a un constructor para el objeto temporal. De manera similar, se llamará al destructor para un temporal con un destructor no trivial (12.4). Los objetos temporales se destruyen como el último paso para evaluar la expresión completa (1.9) que (léxicamente) contiene el punto donde se crearon. Esto es cierto incluso si esa evaluación termina lanzando una excepción.

Creo que esto es una pista falsa, debido a §1.9 / 13:

[Nota: ciertos contextos en C ++ provocan la evaluación de una expresión completa que resulta de un constructo sintáctico distinto de la expresión (5.18). Por ejemplo, en 8.5 una sintaxis para el inicializador es

( expression-list )

pero la construcción resultante es una función llamada a una función constructora con expresión-lista como una lista de argumentos; tal llamada de función es una expresión completa. Por ejemplo, en 8.5, otra sintaxis para el inicializador es

= initializer-clause

pero nuevamente, la construcción resultante podría ser una función llamada a una función constructora con una expresión-asignación como un argumento; de nuevo, la llamada a la función es una expresión completa . ]

Esto me implica que A(b).yield() es en sí misma una expresión completa, lo que hace que el §12.2 / 3 sea irrelevante aquí.

Luego entramos en puntos de secuencia - §1.9 / 7:

Acceder a un objeto designado por un lvalor volátil (3.10), modificar un objeto, llamar a una función de E / S de la biblioteca o llamar a una función que realiza cualquiera de esas operaciones son todos efectos secundarios, que son cambios en el estado del entorno de ejecución. La evaluación de una expresión puede producir efectos secundarios. En ciertos puntos especificados en la secuencia de ejecución llamados puntos de secuencia, todos los efectos secundarios de las evaluaciones previas deberán estar completos y no habrá efectos secundarios de las evaluaciones subsiguientes.

§1.9 / 16:

Hay un punto de secuencia en la finalización de la evaluación de cada expresión completa.

y §1.9 / 17:

Al llamar a una función (ya sea que la función esté o no en línea), hay un punto de secuencia después de la evaluación de todos los argumentos de la función (si corresponde) que tiene lugar antes de la ejecución de cualquier expresión o declaración en el cuerpo de la función. También hay un punto de secuencia después de la copia de un valor devuelto y antes de la ejecución de cualquier expresión fuera de la función.

Poniéndolo todo junto, creo que Clang tiene razón y GCC (y MSVC 2010 SP1) están equivocados: el temporal que contiene el resultado de la expresión (cuya vida útil se extiende según §12.2 / 4) es el valor devuelto por A::yield() , no la A temporal en la que se invoca el yield . Teniendo en cuenta el §1.9, debe haber un punto de secuencia después de la llamada a A::yield() durante el cual se destruye la A temporal.


Creo que está permitido imprimir nada verdadero o falso, o por alguna razón no relacionada, nada en absoluto.

La parte verdadera o falsa es (como has dicho), que la destrucción del objeto A temporal no está ordenada con respecto a la inicialización dinámica de b .

La posibilidad de nada en absoluto es porque la inicialización de b no está ordenada con respecto a la creación / inicialización de std::cout ; cuando intenta destruir el temporal, es posible que cout no se haya creado / inicializado todavía, por lo que intentar imprimir algo puede no funcionar en ese momento. [Editar: esto es específico de C ++ 98/03, y no se aplica a C ++ 11.]

Edición: aquí es como yo, al menos, veo la secuencia:

Edit2: Después de releer §12.2 / 4 (una vez más), he cambiado el diagrama de nuevo. §12.2 / 4 dice:

Hay dos contextos en los que los temporales se destruyen en un punto diferente al final de la expresión completa. El primer contexto es cuando una expresión aparece 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. El objeto se inicializa a partir de una copia del temporal; durante esta copia, una implementación puede llamar al constructor de copia muchas veces; el temporal se destruye después de que se haya copiado, antes o cuando finalice la inicialización.

Creo que esta expresión es un inicializador para un declarador que define un objeto, por lo que es necesario inicializar el objeto a partir de una copia del valor de la expresión ( true , en este caso), no directamente del valor devuelto. En el caso de true , esta es probablemente una distinción sin una diferencia, pero creo que el diagrama es técnicamente más preciso tal como está ahora.

Esto también deja bastante claro (creo) que la retención temporal true no tiene que ser destruida al final de la expresión completa, por lo que también he vuelto a dibujar el diagrama para reflejar eso.

Esta sección desapareció en C ++ 0x / C ++ 11, por lo que volví a dibujar el diagrama (una vez más) para mostrar la diferencia entre los dos (y cuánto más simple se ha conseguido esta pieza en C ++ 11) .


Primero, solo para aclarar el párrafo que estaba aquí anteriormente, el uso de b en su propia inicialización (dinámica) aquí no es UB. Antes de que se evalúe la expresión, b no está sin inicializar, sino que tiene cero inicialización.

La A temporal debe vivir precisamente mientras la expresión completa:

Los objetos temporales se destruyen como el último paso para evaluar la expresión completa (1.9) que (léxicamente) contiene el punto donde se crearon.

[ISO / IEC 14882: 2003 (E) 12.2 / 3]

La línea bool b = A(b).yield(); es una declaración, que es una declaración, que no es una expresión. La expresión disponible solo se encuentra en la RHS de = . [ISO / IEC 14882: 2003 (E) A.6]

Esto significaría que lo temporal debería ser destruido antes de que tenga lugar la inicialización dinámica, ¿no? Claro, el valor true se mantiene en el temporal que contiene el resultado de la expresión 1 hasta que se completa la inicialización, pero el temporal original A debe destruirse antes de que b se modifique realmente.

Por lo tanto esperaría la salida false , cada vez.

1

El primer contexto es cuando una expresión aparece 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 "

[ISO / IEC 14882: 2003 (E) 12.2 / 4]