initialize enclosed brace c++ c++11 initializer-list lifetime brace-initialization

enclosed - initialize vector c++ 11



tiempo de vida de un valor de retorno std:: initializer_list (2)

La implementación de GCC destruye una matriz std::initializer_list devuelta desde una función al final de la expresión completa de retorno. ¿Es esto correcto?

Ambos casos de prueba en este programa muestran los destructores que se ejecutan antes de que se pueda usar el valor:

#include <initializer_list> #include <iostream> struct noisydt { ~noisydt() { std::cout << "destroyed/n"; } }; void receive( std::initializer_list< noisydt > il ) { std::cout << "received/n"; } std::initializer_list< noisydt > send() { return { {}, {}, {} }; } int main() { receive( send() ); std::initializer_list< noisydt > && il = send(); receive( il ); }

Creo que el programa debería funcionar. Pero el standardese subyacente es un poco complicado.

La declaración de retorno inicializa un objeto de valor de retorno como si se declarara

std::initializer_list< noisydt > ret = { {},{},{} };

Esto inicializa un initializer_list temporal y su almacenamiento de matriz subyacente de la serie dada de inicializadores, luego inicializa otro initializer_list desde el primero. ¿Cuál es la vida útil de la matriz? "La vida útil de la matriz es la misma que la del objeto initializer_list ". Pero hay dos de esos; cual es ambiguo El ejemplo en 8.5.4 / 6, si funciona como se anuncia, debería resolver la ambigüedad de que la matriz tiene la vida útil del objeto copiado. Luego, la matriz del valor de retorno también debería sobrevivir en la función de llamada, y debería ser posible conservarla enlazándola a una referencia nombrada.

En LWS , GCC mata erróneamente la matriz antes de regresar, pero conserva una lista de initializer_list nombrada según el ejemplo. Clang también procesa el ejemplo correctamente, pero los objetos en la lista nunca se destruyen; Esto causaría una pérdida de memoria. ICC no soporta initializer_list en absoluto.

¿Mi análisis es correcto?

C ++ 11 §6.6.3 / 2:

Una declaración de retorno con una lista-iniciada con bridas inicializa el objeto o la referencia que se devolverá desde la función mediante la inicialización de la lista de copias (8.5.4) de la lista de inicialización especificada.

8.5.4 / 1:

… La inicialización de lista en un contexto de inicialización de copia se denomina inicialización de lista de copia .

8.5 / 14:

La inicialización que se produce en la forma T x = a; ... se llama inicialización de copia .

De vuelta a 8.5.4 / 3:

La inicialización de lista de un objeto o referencia de tipo T se define de la siguiente manera:…

- De lo contrario, si T es una especialización de std::initializer_list<E> , un objeto initializer_list se construye como se describe a continuación y se usa para inicializar el objeto de acuerdo con las reglas para la inicialización de un objeto de una clase del mismo tipo (8.5) .

8.5.4 / 5:

Un objeto de tipo std::initializer_list<E> se construye a partir de una lista de inicializadores como si la implementación asignara una matriz de N elementos de tipo E , donde N es el número de elementos en la lista de inicializadores. Cada elemento de esa matriz se inicializa con copia con el elemento correspondiente de la lista de inicializadores, y el objeto std::initializer_list<E> se construye para referirse a esa matriz. Si se requiere una conversión de reducción para inicializar cualquiera de los elementos, el programa está mal formado.

8.5.4 / 6:

La vida útil de la matriz es la misma que la del objeto initializer_list . [Ejemplo:

typedef std::complex<double> cmplx; std::vector<cmplx> v1 = { 1, 2, 3 }; void f() { std::vector<cmplx> v2{ 1, 2, 3 }; std::initializer_list<int> i3 = { 1, 2, 3 }; }

Para v1 y v2 , el objeto initiallist_list y la matriz creados para { 1, 2, 3 } tienen una vida de expresión completa. Para i3 , el objeto initializer_list y la matriz tienen una duración automática. - ejemplo final]

Un poco de aclaración acerca de devolver una lista de iniciaciones

Cuando devuelves una lista completa incluida entre llaves,

Una declaración de retorno con una lista-iniciada con bridas inicializa el objeto o la referencia que se devolverá desde la función mediante la inicialización de la lista de copias (8.5.4) de la lista de inicialización especificada.

Esto no implica que el objeto devuelto al ámbito de llamada se copie de algo. Por ejemplo, esto es válido:

struct nocopy { nocopy( int ); nocopy( nocopy const & ) = delete; nocopy( nocopy && ) = delete; }; nocopy f() { return { 3 }; }

esto no es:

nocopy f() { return nocopy{ 3 }; }

Copy-list-initialization simplemente significa que el equivalente de la sintaxis nocopy X = { 3 } se usa para inicializar el objeto que representa el valor de retorno. Esto no invoca una copia, y resulta que es idéntico al ejemplo 8.5.4 / 6 de la extensión de la vida útil de una matriz.

Y Clang y GCC agree de agree en este punto.

Otras notas

Una revisión de N2640 no N2640 ninguna mención de este caso de esquina. Ha habido una extensa discusión sobre las características individuales combinadas aquí, pero no veo nada sobre su interacción.

La implementación de esto se vuelve complicada ya que se reduce a devolver una matriz opcional de longitud variable por valor. Debido a que std::initializer_list no posee su contenido, la función también debe devolver algo que sí lo hace. Cuando se pasa a una función, esto es simplemente una matriz local de tamaño fijo. Pero en la otra dirección, el VLA debe devolverse en la pila, junto con los punteros de std::initializer_list . Luego, se debe informar a la persona que llama si debe deshacerse de la secuencia (ya sea que estén en la pila o no).

Es muy fácil tropezar con este problema devolviendo una lista iniciada desde una función lambda, como una forma "natural" de devolver algunos objetos temporales sin importar cómo están contenidos.

auto && il = []() -> std::initializer_list< noisydt > { return { noisydt{}, noisydt{} }; }();

De hecho, esto es similar a como llegué aquí. Pero, sería un error omitir el -> trailing-return-type porque la deducción del tipo de retorno lambda solo se produce cuando se devuelve una expresión, y una lista de init con paréntesis no es una expresión.


La redacción a la que se refiere en 8.5.4 / 6 es defectuosa y fue corregida (algo) por DR1290 . En lugar de decir:

La vida útil de la matriz es la misma que la del objeto initializer_list .

... la norma modificada ahora dice:

La matriz tiene la misma duración que cualquier otro objeto temporal (12.2 [class.temporary]), excepto que al inicializar un objeto initializer_list desde la matriz, se extiende la vida útil de la matriz exactamente igual que unir una referencia a una temporal.

Por lo tanto, la redacción de control para el tiempo de vida de la matriz temporal es 12.2 / 5, que dice:

La vida útil de un enlace temporal al valor devuelto en una declaración de retorno de función no se extiende; El temporal se destruye al final de la expresión completa en la declaración de retorno

Por lo tanto, los objetos noisydt se destruyen antes de que la función regrese.

Hasta hace poco, Clang tenía un error que hacía que no pudiera destruir la matriz subyacente para un objeto initializer_list en algunas circunstancias. He arreglado eso para Clang 3.4; La salida para su caso de prueba de Clang trunk es:

destroyed destroyed destroyed received destroyed destroyed destroyed received

... lo cual es correcto, por DR1290.


std::initializer_list no es un contenedor, no lo use para pasar valores y esperar que persistan

DR1290 cambió la redacción, también debe conocer 1565 y 1599 que aún no están listos.

Luego, la matriz del valor de retorno también debería sobrevivir en la función de llamada, y debería ser posible conservarla enlazándola a una referencia nombrada.

No, eso no sigue. La vida útil de la matriz no se sigue extendiendo junto con la lista de initializer_list . Considerar:

struct A { const int& ref; A(const int& i = 0) : ref(i) { } };

La referencia i une a la int temporal, y luego la referencia ref une a ella también, pero eso no prolonga la vida útil de i , todavía está fuera del alcance al final del constructor, dejando una referencia pendiente. No extiendes la vida útil del tiempo subyacente vinculando otra referencia a él.

Su código podría ser más seguro si se aprueba 1565 y usted hace que la copia no sea una referencia, pero ese problema aún está abierto y ni siquiera se ha propuesto una redacción, por no hablar de la experiencia de implementación.

Incluso si se pretende que su ejemplo funcione, la redacción relativa a la vida útil de la matriz subyacente obviamente se está mejorando y tomará un tiempo para que los compiladores implementen cualquier semántica final que se establezca.