web-dev-qa-db-fra.com

Le comité des normes C ++ a-t-il l'intention qu'en C ++ 11, unordered_map détruit ce qu'il insère?

Je viens de perdre trois jours de ma vie à retrouver un bug très étrange où unordered_map :: insert () détruit la variable que vous insérez. Ce comportement très peu évident se produit uniquement dans les compilateurs très récents: j'ai trouvé que clang 3.2-3.4 et GCC 4.8 sont les seuls compilateurs pour démontrer cette "fonctionnalité" ".

Voici du code réduit de ma base de code principale qui illustre le problème:

#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;
}

Comme la plupart des programmeurs C++, je m'attendrais à ce que la sortie ressemble à ceci:

a.second is 0x8c14048
a.second is now 0x8c14048

Mais avec clang 3.2-3.4 et GCC 4.8, j'obtiens ceci à la place:

a.second is 0xe03088
a.second is now 0

Ce qui pourrait ne pas avoir de sens, jusqu'à ce que vous examiniez attentivement les documents pour unordered_map :: insert () sur http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ où la surcharge n ° 2 est :

template <class P> pair<iterator,bool> insert ( P&& val );

Ce qui est une surcharge de mouvement de référence universelle gourmande, consommant tout ce qui ne correspond à aucune des autres surcharges, et construction de déplacement en un value_type. Alors, pourquoi notre code ci-dessus a-t-il choisi cette surcharge, et non la surcharge unordered_map :: value_type comme la plupart s'y attendraient probablement?

La réponse vous regarde en face: unordered_map :: value_type est une paire < const int, std :: shared_ptr> et le compilateur penserait correctement qu'une paire < int, std :: shared_ptr> n'est pas convertible. Par conséquent, le compilateur choisit la surcharge de référence universelle de déplacement, et cela détruit l'original, malgré le programmeur n'utilisant pas std :: move () qui est la convention typique pour indiquer que vous êtes d'accord avec une variable détruite. Par conséquent, le comportement de destruction d'insertion est en fait correct selon la norme C++ 11, et les anciens compilateurs étaient incorrect.

Vous pouvez probablement voir maintenant pourquoi j'ai pris trois jours pour diagnostiquer ce bogue. Ce n'était pas du tout évident dans une grande base de code où le type inséré dans unordered_map était un typedef défini très loin en termes de code source, et il n'était jamais venu à l'esprit de quiconque de vérifier si le typedef était identique à value_type.

Donc, mes questions à Stack Overflow:

  1. Pourquoi les anciens compilateurs ne détruisent-ils pas les variables insérées comme les nouveaux compilateurs? Je veux dire, même GCC 4.7 ne fait pas cela, et c'est assez conforme aux normes.

  2. Ce problème est-il largement connu, car la mise à niveau des compilateurs entraînera sûrement l'arrêt brutal du code qui fonctionnait auparavant?

  3. Le comité des normes C++ a-t-il voulu ce comportement?

  4. Comment suggéreriez-vous que unordered_map :: insert () soit modifié pour donner un meilleur comportement? Je pose cette question car s'il y a un support ici, j'ai l'intention de soumettre ce comportement sous forme de note N au WG21 et de leur demander de mettre en œuvre un meilleur comportement.

113
Niall Douglas

Comme d'autres l'ont souligné dans les commentaires, le constructeur "universel" n'est pas, en fait, censé toujours s'écarter de son argument. Il est supposé se déplacer si l'argument est vraiment une valeur r, et copier s'il s'agit d'une valeur l.

Le comportement, vous observez, qui se déplace toujours, est un bogue dans libstdc ++, qui est maintenant corrigé en fonction d'un commentaire sur la question. Pour les curieux, j'ai jeté un œil aux en-têtes g ++ - 4.8.

bits/stl_map.h, lignes 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, lignes 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)); }

Ce dernier utilise incorrectement std::move où il devrait utiliser std::forward.

82
Brian
template <class P> pair<iterator,bool> insert ( P&& val );

Qui est une surcharge de déplacement de référence universelle gourmande, consommant tout ce qui ne correspond à aucune des autres surcharges, et déplace la construction en un type_valeur.

C'est ce que certains appellent référence universelle , mais c'est vraiment la référence s'effondre . Dans votre cas, où l'argument est une lvalue de type pair<int,shared_ptr<int>> il pas se traduira par l'argument étant une référence rvalue et il ne devrait pas s'en déplacer.

Alors, pourquoi notre code ci-dessus a-t-il choisi cette surcharge, et non la surcharge unordered_map :: value_type comme la plupart s'y attendraient probablement?

Parce que vous, comme beaucoup d'autres personnes auparavant, avez mal interprété le value_type dans le conteneur. Le value_type de *map (qu'il soit commandé ou non) est pair<const K, T>, qui dans votre cas est pair<const int, shared_ptr<int>>. Le type ne correspondant pas élimine la surcharge que vous attendez:

iterator       insert(const_iterator hint, const value_type& obj);