Czy komitet normalizacyjny C ++ ma zamiar, aby w C ++ 11 unordered_map niszczył to, co wstawia?

114

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:

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

  2. Czy ten problem jest powszechnie znany, ponieważ z pewnością aktualizacja kompilatorów spowoduje, że kod, który działał, nagle przestanie działać?

  3. Czy komitet normalizacyjny C ++ miał taki zamiar?

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

Niall Douglas
źródło
10
Tylko dlatego, że używa uniwersalnego odniesienia, nie oznacza, że ​​wstawiona wartość jest zawsze przenoszona - powinno to robić tylko dla rvalues, co anie jest zwykłe . Powinien zrobić kopię. Ponadto to zachowanie całkowicie zależy od standardowej biblioteki, a nie od kompilatora.
Xeo,
10
Wygląda na to, że błąd w implementacji biblioteki
David Rodríguez - dribeas
4
„Dlatego zachowanie niszczenia wstawek jest w rzeczywistości poprawne, zgodnie ze standardem C ++ 11, a starsze kompilatory były niepoprawne”. Przepraszam, ale mylisz się. Z jakiej części standardu C ++ wziąłeś ten pomysł? BTW cplusplus.com nie jest oficjalna.
Ben Voigt,
1
Nie mogę tego odtworzyć w moim systemie i używam 4.9.0 20131223 (experimental)odpowiednio gcc 4.8.2 i . Wynik jest a.second is now 0x2074088 (lub podobny) dla mnie.
47
Był to błąd GCC 57619 , regresja w serii 4.8, która została naprawiona w wersji 4.8.2 w latach 2013-06.
Casey,

Odpowiedzi:

83

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–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, wiersze 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)); }

Ten ostatni nieprawidłowo używa miejsca, w std::movektórym powinien std::forward.

Brian
źródło
11
Clang domyślnie używa libstdc ++, standardowego biblioteki GCC.
Xeo
Używam gcc 4.9 i szukam 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.
Tak, więc myślę, że naprawili błąd.
Brian
@Brian Nie, właśnie sprawdziłem nagłówki systemu. Ta sama rzecz. 4.8.2 przy okazji.
Mój 4.8.1, więc myślę, że został naprawiony między nimi.
Brian
20
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.

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

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?

Ponieważ Ty, podobnie jak wiele innych osób wcześniej, źle zinterpretowałeś value_typezawartość pojemnika. value_typeOd *map(czy uporządkowane lub nieuporządkowane) to pair<const K, T>, co jest w twoim przypadku pair<const int, shared_ptr<int>>. Niezgodny typ eliminuje przeciążenie, którego możesz się spodziewać:

iterator       insert(const_iterator hint, const value_type& obj);
David Rodríguez - dribeas
źródło
Nadal nie rozumiem, dlaczego ten nowy lemat istnieje, „uniwersalne odniesienie”, tak naprawdę nie oznacza niczego konkretnego, ani nie nosi dobrego imienia dla rzeczy, która w praktyce nie jest całkowicie „uniwersalna”. Znacznie lepiej jest zapamiętać niektóre stare reguły zwijania, które są częścią zachowania metajęzykowego C ++, tak jak było od czasu wprowadzenia szablonów w języku, a także nową sygnaturę ze standardu C ++ 11. Jaka jest korzyść z mówienia o tych uniwersalnych odniesieniach? Są już nazwiska i rzeczy, które są wystarczająco dziwne, w std::moveogóle nic nie rusza.
user2485710
2
@ user2485710 Czasami ignorancja jest błogosławieństwem, a posiadanie ogólnego terminu „uniwersalne odniesienie” do wszystkich poprawek dotyczących zwijania referencji i odliczania typów szablonów, które IMHO są dość nieintuicyjne, a także wymóg użycia, std::forwardaby te poprawki wykonały prawdziwą pracę ... Scott Meyers wykonał dobrą robotę, ustalając całkiem proste zasady przekazywania (użycie uniwersalnych odniesień).
Mark Garcia,
1
Moim zdaniem „uniwersalne odniesienie” to wzorzec do deklarowania parametrów szablonu funkcji, które mogą być powiązane zarówno z lvalues, jak i rvalues. „Zapadanie się referencji” jest tym, co dzieje się podczas podstawiania (wywnioskowanych lub określonych) parametrów szablonu w definicje, w sytuacjach „uniwersalnego odniesienia” i w innych kontekstach.
aschepler
2
@aschepler: uniwersalne odwołanie to po prostu wymyślna nazwa podzbioru zwijanych referencji. Zgadzam się z tym, że ignorancja to błogość , a także fakt, że posiadanie wymyślnej nazwy sprawia, że ​​mówienie o niej jest łatwiejsze i modniejsze, a to może pomóc w propagowaniu tego zachowania. To powiedziawszy, nie jestem wielkim fanem nazwy, ponieważ spycha ona rzeczywiste zasady do rogu, w którym nie trzeba ich znać ... to znaczy, dopóki nie trafisz na przypadek spoza tego, co opisał Scott Meyers.
David Rodríguez - dribeas
Bezcelowa semantyka, ale rozróżnienie, które próbowałem zasugerować: Powszechne odniesienie ma miejsce, gdy projektuję i deklaruję parametr funkcji jako parametr-szablon-dedukowalny &&; 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.
aschepler