Acabo de perder tres días de mi vida rastreando un error muy extraño en el que unordered_map :: insert () destruye la variable que inserta. Este comportamiento muy poco obvio ocurre solo en compiladores muy recientes: encontré que clang 3.2-3.4 y GCC 4.8 son los únicos compiladores que demuestran esta "característica".
Aquí hay un código reducido de mi base de código principal que demuestra el problema:
#include <memory>
#include <unordered_map>
#include <iostream>
int main(void)
{
std::unordered_map<int, std::shared_ptr<int>> map;
auto a(std::make_pair(5, std::make_shared<int>(5)));
std::cout << "a.second is " << a.second.get() << std::endl;
map.insert(a); // Note we are NOT doing insert(std::move(a))
std::cout << "a.second is now " << a.second.get() << std::endl;
return 0;
}
Yo, como probablemente la mayoría de los programadores de C ++, esperaría que la salida se viera así:
a.second is 0x8c14048
a.second is now 0x8c14048
Pero con clang 3.2-3.4 y GCC 4.8 obtengo esto en su lugar:
a.second is 0xe03088
a.second is now 0
Lo que podría no tener sentido, hasta que examine de cerca los documentos de unordered_map :: insert () en http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ donde la sobrecarga no 2 es:
template <class P> pair<iterator,bool> insert ( P&& val );
Lo cual es una sobrecarga de movimiento de referencia universal codiciosa, que consume cualquier cosa que no coincida con ninguna de las otras sobrecargas y se mueve construyéndolo en un value_type. Entonces, ¿por qué nuestro código anterior eligió esta sobrecarga y no la sobrecarga unordered_map :: value_type como probablemente la mayoría esperaría?
La respuesta te mira a la cara: unordered_map :: value_type es un par < const int, std :: shared_ptr> y el compilador pensaría correctamente que un par < int , std :: shared_ptr> no es convertible. Por lo tanto, el compilador elige la sobrecarga de referencia universal de movimiento, y eso destruye el original, a pesar de que el programador no usa std :: move (), que es la convención típica para indicar que está de acuerdo con la destrucción de una variable. Por lo tanto, el comportamiento de destrucción de inserciones es correcto según el estándar C ++ 11, y los compiladores más antiguos eran incorrectos .
Probablemente pueda ver ahora por qué tardé tres días en diagnosticar este error. No era del todo obvio en una base de código grande donde el tipo que se insertaba en unordered_map era un typedef definido muy lejos en términos de código fuente, y nunca se le ocurrió a nadie verificar si el typedef era idéntico a value_type.
Entonces, mis preguntas para Stack Overflow:
¿Por qué los compiladores más antiguos no destruyen las variables insertadas como los compiladores más nuevos? Quiero decir, incluso GCC 4.7 no hace esto, y se ajusta bastante a los estándares.
¿Es este problema ampliamente conocido, porque seguramente la actualización de los compiladores hará que el código que solía funcionar deje de funcionar repentinamente?
¿El comité de estándares de C ++ pretendía este comportamiento?
¿Cómo sugeriría que unordered_map :: insert () se modifique para ofrecer un mejor comportamiento? Pregunto esto porque si hay apoyo aquí, tengo la intención de enviar este comportamiento como una nota N al WG21 y pedirles que implementen un mejor comportamiento.
a
no es normal . Debería hacer una copia. Además, este comportamiento depende totalmente de stdlib, no del compilador.4.9.0 20131223 (experimental)
respectivamente. La salida esa.second is now 0x2074088
(o similar) para mí.Respuestas:
Como otros han señalado en los comentarios, no se supone que el constructor "universal" se desvíe siempre de su argumento. Se supone que debe moverse si el argumento es realmente un valor r, y copiar si es un valor l.
El comportamiento, observa, que siempre se mueve, es un error en libstdc ++, que ahora se corrigió de acuerdo con un comentario sobre la pregunta. Para aquellos curiosos, eché un vistazo a los encabezados g ++ - 4.8.
bits/stl_map.h
, líneas 598-603bits/unordered_map.h
, líneas 365-370Este último está usando incorrectamente
std::move
donde debería estar usandostd::forward
.fuente
libstdc++-v3/include/bits/
. No veo lo mismo. Ya veo{ return _M_h.insert(std::forward<_Pair>(__x)); }
. Podría ser diferente para 4.8, pero aún no lo he comprobado.Eso es lo que algunas personas llaman referencia universal , pero en realidad la referencia se está derrumbando . En su caso, donde el argumento es un valor l de tipo
pair<int,shared_ptr<int>>
, no resultará en que el argumento sea una referencia de valor r y no debería moverse de él.Porque tú, como muchas otras personas antes, malinterpretaste
value_type
el contenido del contenedor. Elvalue_type
de*map
(ordenado o no ordenado) espair<const K, T>
, que en su caso espair<const int, shared_ptr<int>>
. El tipo que no coincide elimina la sobrecarga que podría estar esperando:fuente
std::move
que no mueven nada en absoluto.std::forward
para hacer que ese ajuste haga el trabajo real ... Scott Meyers ha hecho un buen trabajo estableciendo reglas bastante sencillas para el reenvío (el uso de referencias universales).&&
; El colapso de referencias ocurre cuando un compilador crea una instancia de una plantilla. El colapso de referencias es la razón por la que las referencias universales funcionan, pero a mi cerebro no le gusta poner los dos términos en el mismo dominio.