Ich habe gerade drei Tage meines Lebens verloren, als ich einen sehr seltsamen Fehler aufgespürt habe, bei dem unordered_map :: insert () die von Ihnen eingefügte Variable zerstört. Dieses höchst nicht offensichtliche Verhalten tritt nur bei neueren Compilern auf: Ich fand, dass Clang 3.2-3.4 und GCC 4.8 die einzigen Compiler sind, die diese "Funktion" demonstrieren.
Hier ist ein reduzierter Code aus meiner Hauptcodebasis, der das Problem demonstriert:
#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;
}
Ich würde, wie wahrscheinlich die meisten C ++ - Programmierer, erwarten, dass die Ausgabe ungefähr so aussieht:
a.second is 0x8c14048
a.second is now 0x8c14048
Aber mit Clang 3.2-3.4 und GCC 4.8 bekomme ich stattdessen Folgendes:
a.second is 0xe03088
a.second is now 0
Was möglicherweise keinen Sinn ergibt , bis Sie die Dokumente auf unordered_map :: insert () unter http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ genau untersuchen, wobei Überladung Nr. 2 lautet:
template <class P> pair<iterator,bool> insert ( P&& val );
Dies ist eine gierige universelle Referenzverschiebungsüberladung, die alles verbraucht, was nicht mit einer der anderen Überladungen übereinstimmt, und die Konstruktion in einen value_type verschiebt. Warum hat unser Code oben diese Überladung gewählt und nicht die Überladung unordered_map :: value_type, wie es wahrscheinlich die meisten erwarten würden?
Die Antwort starrt Sie ins Gesicht: unordered_map :: value_type ist ein Paar < const int, std :: shared_ptr> und der Compiler würde richtig denken, dass ein Paar < int , std :: shared_ptr> nicht konvertierbar ist. Daher wählt der Compiler die universelle Referenzüberladung für Verschiebungen, die das Original zerstört, obwohl der Programmierer nicht std :: move () verwendet. Dies ist die typische Konvention, um anzuzeigen, dass Sie mit der Zerstörung einer Variablen einverstanden sind. Daher ist das Verhalten zur Zerstörung von Einfügungen gemäß dem C ++ 11-Standard tatsächlich korrekt , und ältere Compiler waren falsch .
Sie können jetzt wahrscheinlich sehen, warum ich drei Tage gebraucht habe, um diesen Fehler zu diagnostizieren. In einer großen Codebasis, in der der in unordered_map eingefügte Typ ein im Quellcode weit entfernt definierter Typedef war, war dies überhaupt nicht offensichtlich, und es kam niemandem in den Sinn, zu überprüfen, ob der Typedef mit value_type identisch war.
Also meine Fragen an Stack Overflow:
Warum zerstören ältere Compiler keine Variablen, die wie neuere Compiler eingefügt wurden? Ich meine, sogar GCC 4.7 macht das nicht und es entspricht ziemlich den Standards.
Ist dieses Problem allgemein bekannt, weil das Aktualisieren von Compilern dazu führt, dass Code, der früher funktionierte, plötzlich nicht mehr funktioniert?
Hat das C ++ - Standardkomitee dieses Verhalten beabsichtigt?
Wie würden Sie vorschlagen, dass unordered_map :: insert () geändert wird, um ein besseres Verhalten zu erzielen? Ich frage dies, denn wenn es hier Unterstützung gibt, beabsichtige ich, dieses Verhalten als N-Hinweis an WG21 zu senden und sie zu bitten, ein besseres Verhalten zu implementieren.
a
nicht der Fall ist. Es sollte eine Kopie machen. Außerdem hängt dieses Verhalten vollständig von der stdlib ab, nicht vom Compiler.4.9.0 20131223 (experimental)
bzw. Die Ausgabe ista.second is now 0x2074088
(oder ähnlich) für mich.Antworten:
Wie andere in den Kommentaren ausgeführt haben, sollte sich der "universelle" Konstruktor nicht immer von seiner Argumentation entfernen. Es soll sich bewegen, wenn das Argument wirklich ein Wert ist, und kopieren, wenn es ein Wert ist.
Das Verhalten, das Sie beobachten und das sich immer bewegt, ist ein Fehler in libstdc ++, der jetzt gemäß einem Kommentar zu der Frage behoben wird. Für die Neugierigen habe ich mir die g ++ - 4.8 Header angesehen.
bits/stl_map.h
Zeilen 598-603bits/unordered_map.h
, Zeilen 365-370Letzteres verwendet falsch,
std::move
wo es verwendet werden solltestd::forward
.quelle
libstdc++-v3/include/bits/
. Ich sehe nicht dasselbe. Ich verstehe{ return _M_h.insert(std::forward<_Pair>(__x)); }
. Bei 4.8 könnte es anders sein, aber ich habe es noch nicht überprüft.Das ist es, was manche Leute als universelle Referenz bezeichnen , aber es ist wirklich ein Zusammenbruch der Referenz . In Ihrem Fall, wenn das Argument ein l-Wert vom Typ ist
pair<int,shared_ptr<int>>
, führt dies nicht dazu, dass das Argument eine r-Wert-Referenz ist, und es sollte nicht von diesem verschoben werden.Weil Sie, wie viele andere Menschen zuvor, das
value_type
im Container falsch interpretiert haben . Dasvalue_type
von*map
(ob bestellt oder ungeordnet) istpair<const K, T>
, was in Ihrem Fall istpair<const int, shared_ptr<int>>
. Der nicht übereinstimmende Typ beseitigt die erwartete Überlastung:quelle
std::move
dass sich überhaupt nichts bewegt.std::forward
nach ziemlich unintuitiv, plus die Anforderung , diese Optimierung für die eigentliche Arbeit zu verwenden ... Scott Meyers hat gute Arbeit geleistet und ziemlich einfache Regeln für die Weiterleitung festgelegt (Verwendung universeller Referenzen).&&
; Das Reduzieren von Referenzen erfolgt, wenn ein Compiler eine Vorlage instanziiert. Das Zusammenfallen von Referenzen ist der Grund, warum universelle Referenzen funktionieren, aber mein Gehirn mag es nicht, die beiden Begriffe in dieselbe Domäne zu setzen.