Ist es vom C ++ - Standardkomitee beabsichtigt, dass unordered_map in C ++ 11 zerstört, was es einfügt?

114

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:

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

  2. Ist dieses Problem allgemein bekannt, weil das Aktualisieren von Compilern dazu führt, dass Code, der früher funktionierte, plötzlich nicht mehr funktioniert?

  3. Hat das C ++ - Standardkomitee dieses Verhalten beabsichtigt?

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

Niall Douglas
quelle
10
Nur weil ein universeller Verweis verwendet wird, bedeutet dies nicht, dass der eingefügte Wert immer verschoben wird - dies sollte immer nur für r-Werte geschehen, was anicht der Fall ist. Es sollte eine Kopie machen. Außerdem hängt dieses Verhalten vollständig von der stdlib ab, nicht vom Compiler.
Xeo
10
Das scheint ein Fehler in der Implementierung der Bibliothek zu sein
David Rodríguez - dribeas
4
"Daher ist das Verhalten zur Zerstörung von Einfügungen gemäß dem C ++ 11-Standard tatsächlich korrekt, und ältere Compiler waren falsch." Entschuldigung, aber du liegst falsch. Aus welchem ​​Teil des C ++ - Standards sind Sie auf diese Idee gekommen? Übrigens ist cplusplus.com nicht offiziell.
Ben Voigt
1
Ich kann dies auf meinem System nicht reproduzieren und verwende gcc 4.8.2 4.9.0 20131223 (experimental)bzw. Die Ausgabe ist a.second is now 0x2074088 (oder ähnlich) für mich.
47
Dies war der GCC-Fehler 57619 , eine Regression in der 4.8-Serie, die 2013-06 für 4.8.2 behoben wurde.
Casey

Antworten:

83

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.hZeilen 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, Zeilen 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)); }

Letzteres verwendet falsch, std::movewo es verwendet werden sollte std::forward.

Brian
quelle
11
Clang verwendet standardmäßig libstdc ++, die stdlib von GCC.
Xeo
Ich benutze gcc 4.9 und schaue rein 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.
Ja, ich denke, sie haben den Fehler behoben.
Brian
@ Brian Nein, ich habe gerade meine Systemheader überprüft. Gleiche Sache. 4.8.2 übrigens.
Mine ist 4.8.1, also denke ich, dass es zwischen den beiden behoben wurde.
Brian
20
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.

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.

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?

Weil Sie, wie viele andere Menschen zuvor, das value_typeim Container falsch interpretiert haben . Das value_typevon *map(ob bestellt oder ungeordnet) ist pair<const K, T>, was in Ihrem Fall ist pair<const int, shared_ptr<int>>. Der nicht übereinstimmende Typ beseitigt die erwartete Überlastung:

iterator       insert(const_iterator hint, const value_type& obj);
David Rodríguez - Dribeas
quelle
Ich verstehe immer noch nicht, warum dieses neue Lemma existiert, "universelle Referenz", nicht wirklich etwas Spezifisches bedeutet, noch einen guten Namen für eine Sache trägt, die in der Praxis überhaupt nicht "universell" ist. Es ist viel besser, sich an einige alte Regeln zu erinnern, die Teil des C ++ - Metasprachenverhaltens sind, wie es seit Einführung der Vorlagen in der Sprache war, sowie an eine neue Signatur aus dem C ++ 11-Standard. Was bringt es überhaupt, über diese universellen Referenzen zu sprechen? Es gibt bereits Namen und Dinge, die seltsam genug sind, so std::movedass sich überhaupt nichts bewegt.
user2485710
2
@ user2485710 Manchmal ist Unwissenheit Glückseligkeit, und der Überbegriff "universelle Referenz" für alle Anpassungen, die zusammenfallen und vom Typ des Vorlagenabzugs sind, ist meiner Meinung std::forwardnach 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).
Mark Garcia
1
In meinem Denken ist "universelle Referenz" ein Muster zum Deklarieren von Funktionsvorlagenparametern, die sowohl an l-Werte als auch an r-Werte binden können. "Referenzkollabieren" ist das, was passiert, wenn Vorlagenparameter (abgeleitete oder spezifizierte) in Definitionen, in Situationen mit "universeller Referenz" und in anderen Kontexten eingesetzt werden.
Aschepler
2
@aschepler: Universal Reference ist nur ein ausgefallener Name für eine Teilmenge von Referenzkollaps. Ich stimme der Unwissenheit zu, die Glückseligkeit ist , und auch der Tatsache, dass ein ausgefallener Name das Sprechen einfacher und trendiger macht und dies dazu beitragen könnte, das Verhalten zu verbreiten. Davon abgesehen bin ich kein großer Fan des Namens, da er die tatsächlichen Regeln in eine Ecke treibt, in der sie nicht bekannt sein müssen ... das heißt, bis Sie einen Fall außerhalb dessen treffen, was Scott Meyers beschrieben hat.
David Rodríguez - Dribeas
Jetzt sinnlose Semantik, aber die Unterscheidung, die ich vorschlagen wollte: Universelle Referenz tritt auf, wenn ich einen Funktionsparameter als deduzierbaren Vorlagenparameter entwerfe und deklariere &&; 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.
Aschepler