¿El comité de estándares de C ++ pretende que en C ++ 11 unordered_map destruya lo que inserta?

114

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:

  1. ¿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.

  2. ¿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?

  3. ¿El comité de estándares de C ++ pretendía este comportamiento?

  4. ¿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.

Niall Douglas
fuente
10
El hecho de que use una referencia universal no significa que el valor insertado siempre se mueva; solo debería hacerlo para rvalues, lo que ano es normal . Debería hacer una copia. Además, este comportamiento depende totalmente de stdlib, no del compilador.
Xeo
10
Eso parece un error en la implementación de la biblioteca
David Rodríguez - dribeas
4
"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". Lo siento, pero te equivocas. ¿De qué parte del estándar C ++ sacaste esa idea? Por cierto, cplusplus.com no es oficial.
Ben Voigt
1
No puedo reproducir esto en mi sistema y estoy usando gcc 4.8.2 y 4.9.0 20131223 (experimental)respectivamente. La salida es a.second is now 0x2074088 (o similar) para mí.
47
Este fue el error de GCC 57619 , una regresión en la serie 4.8 que se corrigió para 4.8.2 en 2013-06.
Casey

Respuestas:

83

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-603

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_t._M_insert_unique(std::forward<_Pair>(__x)); }

bits/unordered_map.h, líneas 365-370

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_h.insert(std::move(__x)); }

Este último está usando incorrectamente std::movedonde debería estar usando std::forward.

Brian
fuente
11
Clang usa libstdc ++ por defecto, stdlib de GCC.
Xeo
Estoy usando gcc 4.9 y estoy buscando 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.
Sí, supongo que arreglaron el error.
Brian
@Brian No, acabo de comprobar los encabezados de mi sistema. La misma cosa. 4.8.2 por cierto.
El mío es 4.8.1, así que supongo que se solucionó entre los dos.
Brian
20
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.

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.

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?

Porque tú, como muchas otras personas antes, malinterpretaste value_typeel contenido del contenedor. El value_typede *map(ordenado o no ordenado) es pair<const K, T>, que en su caso es pair<const int, shared_ptr<int>>. El tipo que no coincide elimina la sobrecarga que podría estar esperando:

iterator       insert(const_iterator hint, const value_type& obj);
David Rodríguez - dribeas
fuente
Todavía no entiendo por qué existe este nuevo lema, "referencia universal", en realidad no significa nada específico, ni tiene un buen nombre para algo que no es totalmente "universal" en la práctica. Es mucho mejor recordar algunas reglas de colapso antiguas que son parte del comportamiento del metalenguaje de C ++ como lo era desde que se introdujeron las plantillas en el lenguaje, además de una nueva firma del estándar C ++ 11. ¿Cuál es el beneficio de hablar de estas referencias universales de todos modos? Ya hay nombres y cosas que son bastante raras, como std::moveque no mueven nada en absoluto.
user2485710
2
@ user2485710 A veces, la ignorancia es una bendición, y tener el término general "referencia universal" en todo el colapso de referencia y el ajuste de deducción del tipo de plantilla es, en mi humilde opinión, bastante poco intuitivo, además del requisito de usar std::forwardpara 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).
Mark García
1
En mi opinión, la "referencia universal" es un patrón para declarar parámetros de plantilla de función que se pueden vincular tanto a lvalues ​​como a rvalues. El "colapso de referencias" es lo que sucede cuando se sustituyen (deducidos o especificados) parámetros de plantilla en definiciones, en situaciones de "referencia universal" y en otros contextos.
aschepler
2
@aschepler: la referencia universal es solo un nombre elegante para un subconjunto de colapso de referencia. Estoy de acuerdo con que la ignorancia es una bendición , y también con el hecho de que tener un nombre elegante hace que hablar sobre él sea más fácil y más moderno y eso podría ayudar a propagar el comportamiento. Dicho esto, no soy un gran admirador del nombre, ya que lleva las reglas reales a un rincón donde no es necesario conocerlas ... es decir, hasta que encuentras un caso fuera de lo que describió Scott Meyers.
David Rodríguez - dribeas
Semántica sin sentido ahora, pero la distinción que estaba tratando de sugerir: la referencia universal ocurre cuando estoy diseñando y declaro un parámetro de función como parámetro de plantilla deducible &&; 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.
aschepler