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

114

Je viens de perdre trois jours de ma vie à traquer 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 à démontrer cette "fonctionnalité".

Voici un 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 à quelque chose comme 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 de près la documentation pour unordered_map :: insert () à 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 déplacement de référence universelle gourmande, consommant tout ce qui ne correspond à aucune des autres surcharges, et déplacez-le en le construisant dans 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, bien que le programmeur n'utilise 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'insert est en fait correct selon la norme C ++ 11 et les anciens compilateurs étaient incorrects .

Vous pouvez probablement voir maintenant pourquoi j'ai mis trois jours pour diagnostiquer ce bogue. Ce n'était pas du tout évident dans une base de code volumineuse où le type inséré dans unordered_map était un typedef défini loin en termes de code source, et personne n'a jamais pensé à 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 le fait pas, 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 soudain du code qui fonctionnait auparavant?

  3. Le comité des normes C ++ avait-il l'intention de ce comportement?

  4. Comment suggéreriez-vous que unordered_map :: insert () soit modifié pour donner un meilleur comportement? Je demande cela parce que s'il y a du soutien 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.

Niall Douglas
la source
10
Ce n'est pas parce qu'il utilise une référence universelle que la valeur insérée est toujours déplacée - il ne devrait le faire que pour rvalues, ce qui an'est pas le cas. Il devrait en faire une copie. En outre, ce comportement dépend totalement du stdlib, pas du compilateur.
Xeo
10
Cela semble être un bug dans l'implémentation de la bibliothèque
David Rodríguez - Dribeas
4
"Par conséquent, le comportement de destruction d'insert est en fait correct selon la norme C ++ 11, et les anciens compilateurs étaient incorrects." Désolé, mais vous vous trompez. De quelle partie du standard C ++ avez-vous eu cette idée? BTW cplusplus.com n'est pas officiel.
Ben Voigt
1
Je ne peux pas reproduire cela sur mon système et j'utilise 4.9.0 20131223 (experimental)respectivement gcc 4.8.2 et . La sortie est a.second is now 0x2074088 (ou similaire) pour moi.
47
Il s'agissait du bogue GCC 57619 , une régression de la série 4.8 qui a été corrigée pour 4.8.2 en 2013-06.
Casey

Réponses:

83

Comme d'autres l'ont souligné dans les commentaires, le constructeur "universel" n'est pas, en fait, censé toujours s'éloigner de son argument. Il est censé bouger si l'argument est vraiment une rvalue, et copier si c'est une lvalue.

Le comportement, vous observez, qui bouge 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 coup d'œ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::movelà où il devrait être utilisé std::forward.

Brian
la source
11
Clang utilise par défaut libstdc ++, le stdlib de GCC.
Xeo
J'utilise gcc 4.9 et je cherche libstdc++-v3/include/bits/. Je ne vois pas la même chose. Je vois { return _M_h.insert(std::forward<_Pair>(__x)); }. Cela pourrait être différent pour 4.8, mais je n'ai pas encore vérifié.
Ouais, donc je suppose qu'ils ont corrigé le bug.
Brian
@Brian Non, je viens de vérifier mes en-têtes système. Même chose. 4.8.2 btw.
Le mien est 4.8.1, donc je suppose qu'il a été fixé entre les deux.
Brian
20
template <class P> pair<iterator,bool> insert ( P&& val );

Ce 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éplacez-le en le construisant dans un value_type.

C'est ce que certains appellent la référence universelle , mais c'est en réalité l' effondrement des références . Dans votre cas, lorsque l'argument est une lvalue de type , l'argument pair<int,shared_ptr<int>>ne sera pas une référence rvalue et il ne doit pas en sortir .

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é value_typele contenu du conteneur. Le value_typede *map(qu'il soit commandé ou non) est pair<const K, T>, ce qui dans votre cas est pair<const int, shared_ptr<int>>. Le type qui ne correspond pas élimine la surcharge à laquelle vous pourriez vous attendre:

iterator       insert(const_iterator hint, const value_type& obj);
David Rodríguez - Dribeas
la source
Je ne comprends toujours pas pourquoi ce nouveau lemme existe, «référence universelle», ne veut vraiment rien dire de spécifique, ni ne porte un bon nom pour une chose qui n'est pas du tout «universelle» dans la pratique. Il est préférable de se souvenir de certaines anciennes règles de réduction qui font partie du comportement du méta-langage C ++ tel qu'il était depuis l'introduction des modèles dans le langage, plus une nouvelle signature du standard C ++ 11. Quel est l'intérêt de parler de ces références universelles de toute façon? Il y a déjà des noms et des choses assez bizarres, comme std::moveça ne bouge rien du tout.
user2485710
2
@ user2485710 Parfois, l'ignorance est un bonheur, et avoir le terme générique «référence universelle» sur tous les ajustements de réduction de référence et de déduction de type de modèle qui, à mon humble avis, est assez peu intuitif, plus l'obligation d'utiliser std::forwardpour que ce réglage fasse le vrai travail ... Scott Meyers a fait du bon travail en définissant des règles assez simples pour le transfert (l'utilisation de références universelles).
Mark Garcia
1
Dans ma pensée, "référence universelle" est un modèle pour déclarer des paramètres de modèle de fonction qui peuvent se lier à la fois à lvalues ​​et à rvalues. La "réduction de référence" est ce qui se produit lors de la substitution de paramètres de modèle (déduits ou spécifiés) dans des définitions, dans des situations de "référence universelle" et dans d'autres contextes.
aschepler
2
@aschepler: la référence universelle est juste un nom sophistiqué pour un sous-ensemble de réduction de référence. Je suis d'accord avec le fait que l' ignorance est le bonheur , et aussi le fait qu'avoir un nom sophistiqué rend plus facile et plus tendance d'en parler et cela pourrait aider à propager le comportement. Cela étant dit, je ne suis pas un grand fan du nom, car il pousse les règles réelles dans un coin où elles n'ont pas besoin d'être connues ... c'est-à-dire jusqu'à ce que vous frappiez un cas en dehors de ce que Scott Meyers a décrit.
David Rodríguez - dribeas le
Sémantique inutile maintenant, mais la distinction que j'essayais de suggérer: la référence universelle se produit lorsque je conçois et déclare un paramètre de fonction comme paramètre de modèle déductible &&; la réduction des références se produit lorsqu'un compilateur instancie un modèle. L'effondrement des références est la raison pour laquelle les références universelles fonctionnent, mais mon cerveau n'aime pas mettre les deux termes dans le même domaine.
aschepler