Właśnie straciłem trzy dni mojego życia, szukając bardzo dziwnego błędu, w którym unordered_map :: insert () niszczy wstawioną zmienną. To wysoce nieoczywiste zachowanie występuje tylko w najnowszych kompilatorach: stwierdziłem, że Clang 3.2-3.4 i GCC 4.8 są jedynymi kompilatorami, które zademonstrowały tę „funkcję”.
Oto trochę zredukowany kod z mojej głównej bazy kodu, który demonstruje problem:
#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;
}
Ja, podobnie jak prawdopodobnie większość programistów C ++, spodziewałbym się, że wynik będzie wyglądał mniej więcej tak:
a.second is 0x8c14048
a.second is now 0x8c14048
Ale z clang 3.2-3.4 i GCC 4.8 otrzymuję to:
a.second is 0xe03088
a.second is now 0
Co może nie mieć sensu, dopóki nie przyjrzysz się dokładnie dokumentom dla unordered_map :: insert () pod adresem http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/, gdzie przeciążenie nr 2 to:
template <class P> pair<iterator,bool> insert ( P&& val );
Co jest chciwym, uniwersalnym przeciążeniem ruchu referencyjnego, zużywającym wszystko, co nie pasuje do żadnego z innych przeciążeń, i przenosząc konstruowanie go do value_type. Dlaczego więc nasz kod powyżej wybrał to przeciążenie, a nie przeciążenie unordered_map :: value_type, jak prawdopodobnie większość się spodziewała?
Odpowiedź patrzy ci prosto w twarz: unordered_map :: value_type to para < const int, std :: shared_ptr> i kompilator poprawnie pomyśli, że para < int , std :: shared_ptr> nie jest konwertowalna. Dlatego kompilator wybiera przeciążenie odwołania uniwersalnego przenoszenia, co niszczy oryginał, mimo że programista nie używa std :: move (), co jest typową konwencją wskazującą, że nie ma problemu z niszczeniem zmiennej. Dlatego zachowanie niszczenia wstawek jest w rzeczywistości poprawne, zgodnie ze standardem C ++ 11, a starsze kompilatory były niepoprawne .
Prawdopodobnie teraz widzisz, dlaczego zdiagnozowanie tego błędu zajęło mi trzy dni. Nie było to wcale oczywiste w dużej bazie kodu, gdzie typ wstawiany do unordered_map był typedef zdefiniowanym daleko w warunkach kodu źródłowego i nigdy nie przyszło nikomu do głowy, aby sprawdzić, czy typedef jest identyczny z value_type.
Więc moje pytania do Stack Overflow:
Dlaczego starsze kompilatory nie niszczą zmiennych wstawionych jak nowsze kompilatory? To znaczy, nawet GCC 4.7 tego nie robi i jest dość zgodne ze standardami.
Czy ten problem jest powszechnie znany, ponieważ z pewnością aktualizacja kompilatorów spowoduje, że kod, który działał, nagle przestanie działać?
Czy komitet normalizacyjny C ++ miał taki zamiar?
Jak sugerowałbyś modyfikację unordered_map :: insert (), aby zapewnić lepsze zachowanie? Pytam o to, ponieważ jeśli jest tu wsparcie, zamierzam przesłać to zachowanie jako notę N do WG21 i poprosić o zaimplementowanie lepszego zachowania.
a
nie jest zwykłe . Powinien zrobić kopię. Ponadto to zachowanie całkowicie zależy od standardowej biblioteki, a nie od kompilatora.4.9.0 20131223 (experimental)
odpowiednio gcc 4.8.2 i . Wynik jesta.second is now 0x2074088
(lub podobny) dla mnie.Odpowiedzi:
Jak zauważyli inni w komentarzach, „uniwersalny” konstruktor nie powinien w rzeczywistości zawsze odchodzić od swojego argumentu. Powinien się przesunąć, jeśli argument jest naprawdę wartością r, i skopiować, jeśli jest lwartością.
Zachowanie, które obserwujesz, które zawsze się porusza, jest błędem w libstdc ++, który został naprawiony zgodnie z komentarzem do pytania. Dla ciekawskich przyjrzałem się nagłówkom g ++ - 4.8.
bits/stl_map.h
, wiersze 598–603bits/unordered_map.h
, wiersze 365–370Ten ostatni nieprawidłowo używa miejsca, w
std::move
którym powinienstd::forward
.źródło
libstdc++-v3/include/bits/
. Nie widzę tego samego. Rozumiem{ return _M_h.insert(std::forward<_Pair>(__x)); }
. Może być inaczej dla 4.8, ale jeszcze nie sprawdziłem.To właśnie niektórzy nazywają uniwersalnym odniesieniem , ale tak naprawdę jest to załamanie się odniesienia . W twoim przypadku, gdy argument jest lwartością typu,
pair<int,shared_ptr<int>>
nie spowoduje to, że argument będzie odwołaniem do r-wartości i nie powinien się z niego ruszyć.Ponieważ Ty, podobnie jak wiele innych osób wcześniej, źle zinterpretowałeś
value_type
zawartość pojemnika.value_type
Od*map
(czy uporządkowane lub nieuporządkowane) topair<const K, T>
, co jest w twoim przypadkupair<const int, shared_ptr<int>>
. Niezgodny typ eliminuje przeciążenie, którego możesz się spodziewać:źródło
std::move
ogóle nic nie rusza.std::forward
aby te poprawki wykonały prawdziwą pracę ... Scott Meyers wykonał dobrą robotę, ustalając całkiem proste zasady przekazywania (użycie uniwersalnych odniesień).&&
; zwijanie odwołań ma miejsce, gdy kompilator tworzy wystąpienie szablonu. Powodem tego, że odwołania uniwersalne działają, jest załamanie się referencji, ale mój mózg nie lubi umieszczać tych dwóch terminów w tej samej dziedzinie.