c++ - examples - Valores extraños en una lambda que devuelve initializer_list
std c++ (3)
Considere este fragmento de código C ++ 11 :
#include <iostream>
#include <set>
#include <stdexcept>
#include <initializer_list>
int main(int argc, char ** argv)
{
enum Switch {
Switch_1,
Switch_2,
Switch_3,
Switch_XXXX,
};
int foo_1 = 1;
int foo_2 = 2;
int foo_3 = 3;
int foo_4 = 4;
int foo_5 = 5;
int foo_6 = 6;
int foo_7 = 7;
auto get_foos = [=] (Switch ss) -> std::initializer_list<int> {
switch (ss) {
case Switch_1:
return {foo_1, foo_2, foo_3};
case Switch_2:
return {foo_4, foo_5};
case Switch_3:
return {foo_6, foo_7};
default:
throw std::logic_error("invalid switch");
}
};
std::set<int> foos = get_foos(Switch_1);
for (auto && foo : foos) {
std::cout << foo << " ";
}
std::cout << std::endl;
return 0;
}
Cualquiera que sea el compilador que intento, todos parecen manejarlo incorrectamente. Esto me hace pensar que estoy haciendo algo mal en lugar de que sea un error común en múltiples compiladores.
salida de clang 3.5 :
-1078533848 -1078533752 134518134
salida de gcc 4.8.2 :
-1078845996 -1078845984 3
salida de gcc 4.8.3 (compilada en http://www.tutorialspoint.com ):
1 2 267998238
salida de gcc (versión desconocida) (compilada en http://coliru.stacked-crooked.com )
-1785083736 0 6297428
El problema parece ser causado por el uso de std::initializer_list<int>
como valor de retorno de lambda. Al cambiar la definición de lambda a [=] (Switch ss) -> std::set<int> {...}
valores devueltos son correctos.
Por favor, ayúdame a resolver este misterio.
De: http://en.cppreference.com/w/cpp/utility/initializer_list
La matriz subyacente no se garantiza que exista después de que haya finalizado la vida útil del objeto original de la lista de inicializadores. El almacenamiento para std :: initializer_list no está especificado (es decir, podría ser automático, temporal o memoria estática de solo lectura, dependiendo de la situación).
No creo que la lista de inicializadores sea de construcción de copia. std::set
y otros contenedores son. Básicamente, parece que su código se comporta de forma similar a "devolver una referencia a un temporal".
C ++ 14 tiene algo ligeramente diferente que decir sobre el almacenamiento subyacente, extendiendo su tiempo de vida, pero eso no soluciona nada que tenga que ver con el tiempo de vida del objeto initializer_list
, y mucho menos con las copias del mismo. Por lo tanto, el problema persiste, incluso en C ++ 14.
El conjunto subyacente es un conjunto temporal, en el que cada elemento se inicializa en copia (excepto que las conversiones de anulación no son válidas) del elemento correspondiente de la lista de inicializadores original. La duración de la matriz subyacente es la misma que cualquier otro objeto temporal, excepto que la inicialización de un objeto initializer_list de la matriz extiende la vida útil de la matriz exactamente igual que vinculando una referencia a una temporal (con las mismas excepciones, como para inicializar una no miembro de la clase estática). La matriz subyacente puede asignarse en la memoria de solo lectura.
El problema es que está haciendo referencia a un objeto que ya no existe y, por lo tanto, está invocando un comportamiento indefinido . initializer_list
parece no estar especificado en el borrador del estándar C ++ 11 , no hay secciones normativas que realmente especifiquen este comportamiento. Aunque hay muchas notas que indican que esto no funcionará y, en general, aunque las notas no son normativas si no entran en conflicto con el texto normativo, son muy indicativas.
Si vamos a la sección 18.9
lista del inicializador tiene una nota que dice:
Copiar una lista de inicializadores no copia los elementos subyacentes.
y en la sección 8.5.4
tenemos los siguientes ejemplos:
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 };
}
con las siguientes notas:
Para v1 y v2, el objeto initializer_list y la matriz creada para {1, 2, 3} tienen un tiempo de vida de expresión plena. Para i3, el objeto initializer_list y la matriz tienen un tiempo de vida automático.
Estas notas son consistentes con la propuesta initializer_list: N2215 que da el siguiente ejemplo:
std::vector<double> v = {1, 2, 3.14};
y dice:
Ahora agregue el
vector(initializer_list<E>)
alvector<E>
como se muestra arriba. Ahora, el ejemplo funciona. La lista de inicializadores {1, 2, 3.14} se interpreta como una construcción temporal como esta:
const double temp[] = {double(1), double(2), 3.14 } ; initializer_list<double> tmp(temp, sizeof(temp)/sizeof(double)); vector<double> v(tmp);
[...]
Tenga en cuenta que initializer_list es un objeto pequeño (probablemente dos palabras), por lo que pasarlo por valor tiene sentido. Pasar por valor también simplifica la incorporación de begin () y end () y la evaluación de expresión constante de size ().
El compilador creará una initializer_list s, pero los usuarios pueden copiarla. Piense en ello como un par de punteros.
initializer_list
en este caso solo contiene punteros a una variable automática que no existirá después de salir del alcance.
Actualizar
Me acabo de dar cuenta de que la propuesta realmente señala este escenario de mal uso :
Una implicación es que initializer_list es "puntero" porque se comporta como un puntero con respecto a la matriz subyacente. Por ejemplo:
int * f(int a) { int* p = &a; return p; //bug waiting to happen } initializer_list<int> g(int a, int b, int c) { initializer_list<int> v = { a, b, c }; return v; // bug waiting to happen }
De hecho, se requiere una pequeña cantidad de ingenio para hacer un mal uso de una lista de inicializadores de esta manera. En particular, las variables de tipo initializer_list van a ser raras .
Encuentro la última afirmación ( énfasis mío ) particularmente irónica.
Actualización 2
Entonces, el informe de defectos 1290 corrige la redacción normativa, por lo que ahora cubre este comportamiento, aunque el caso de copia podría ser más explícito. Dice:
Ha surgido una pregunta sobre el comportamiento esperado cuando un initializer_list es un miembro de datos no estático de una clase. La inicialización de una initializer_list se define en términos de construcción a partir de una matriz implícitamente asignada cuya duración "es la misma que la del objeto initializer_list". Eso significaría que la matriz necesita vivir tanto tiempo como lo hace initializer_list, lo que a primera vista parece requerir que la matriz se almacene en algo así como std :: unique_ptr dentro de la misma clase (si el miembro se inicializa en de esta manera).
Sería sorprendente si esa fuera la intención, pero haría initializer_list utilizable en este contexto.
La resolución corrige la redacción y podemos encontrar la nueva redacción en la versión N3485 del borrador del estándar . Entonces, la sección 8.5.4
[dcl.init.list] ahora dice:
La matriz tiene el mismo tiempo de vida que cualquier otro objeto temporal (12.2), excepto que la inicialización de un objeto initializer_-list desde la matriz extiende la vida útil de la matriz exactamente igual que vinculando una referencia a una temporal.
y 12.2
[class.temporary] dice:
La duración de un límite temporal al valor devuelto en una declaración de devolución de función (6.6.3) no se extiende; el temporal se destruye al final de la expresión completa en la declaración de devolución.
Por lo tanto, initializer_list
s no extiende la vida útil de la matriz a la que se hace referencia cuando se copian o mueven al resultado de la copia / movimiento. Esto hace que devolverlos sea problemático. (amplían la vida útil de la matriz referenciada a su propia vida, pero esta extensión no es transitiva en lugar de elisión o copias de la lista).
Para solucionar este problema, almacene los datos y administre su duración de forma manual:
template<size_t size, class T>
std::array<T, size> partial_array( T const* begin, T const* end ) {
std::array<T, size> retval;
size_t delta = (std::min)( size, end-begin );
end = begin+delta;
std::copy( begin, end, retval.begin() );
return retval;
}
template<class T, size_t max_size>
struct capped_array {
std::array<T, max_size> storage;
size_t used = 0;
template<size_t osize, class=std::enable_if_t< (size<=max_size) >>
capped_array( std::array<T, osize> const& rhs ):
capped_array( rhs.data(), rhs.data()+osize )
{}
template<size_t osize, class=std::enable_if_t< (size<=max_size) >>
capped_array( capped_array<T, osize> const& rhs ):
capped_array( rhs.data(), rhs.data()+rhs.used )
{}
capped_array(capped_array const& o)=default;
capped_array(capped_array & o)=default;
capped_array(capped_array && o)=default;
capped_array(capped_array const&& o)=default;
capped_array& operator=(capped_array const& o)=default;
capped_array& operator=(capped_array & o)=default;
capped_array& operator=(capped_array && o)=default;
capped_array& operator=(capped_array const&& o)=default;
// finish-start MUST be less than max_size, or we will truncate
capped_array( T const* start, T const* finish ):
storage( partial_array(start, finish) ),
used((std::min)(finish-start, size))
{}
T* begin() { return storage.data(); }
T* end() { return storage.data()+used; }
T const* begin() const { return storage.data(); }
T const* end() const { return storage.data()+used; }
size_t size() const { return used; }
bool empty() const { return !used; }
T& front() { return *begin(); }
T const& front() const { return *begin(); }
T& back() { return *std::prev(end()); }
T const& back() const { return *std::prev(end()); }
capped_array( std::initializer_list<T> il ):
capped_array(il.begin(), il.end() )
{}
};
el objetivo aquí es simple. Cree un tipo de datos basado en pila que almacene un montón de T
, hasta un tope, y pueda manejar tener menos.
Ahora reemplazamos su std::initializer_list
por:
auto get_foos = [=] (Switch ss) -> capped_array<int,3> {
switch (ss) {
case Switch_1:
return {foo_1, foo_2, foo_3};
case Switch_2:
return {foo_4, foo_5};
case Switch_3:
return {foo_6, foo_7};
default:
throw std::logic_error("invalid switch");
}
};
y tu código funciona La tienda gratuita no se usa (no hay asignación de montón).
Una versión más avanzada usaría una matriz de datos no inicializados y construiría manualmente cada T