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:
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.
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?
Le comité des normes C ++ avait-il l'intention de ce comportement?
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.
a
n'est pas le cas. Il devrait en faire une copie. En outre, ce comportement dépend totalement du stdlib, pas du compilateur.4.9.0 20131223 (experimental)
respectivement gcc 4.8.2 et . La sortie esta.second is now 0x2074088
(ou similaire) pour moi.Réponses:
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-603bits/unordered_map.h
, lignes 365-370Ce dernier utilise incorrectement
std::move
là où il devrait être utiliséstd::forward
.la source
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é.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 .Parce que vous, comme beaucoup d'autres personnes auparavant, avez mal interprété
value_type
le contenu du conteneur. Levalue_type
de*map
(qu'il soit commandé ou non) estpair<const K, T>
, ce qui dans votre cas estpair<const int, shared_ptr<int>>
. Le type qui ne correspond pas élimine la surcharge à laquelle vous pourriez vous attendre:la source
std::move
ça ne bouge rien du tout.std::forward
pour 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).&&
; 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.