Я только что потерял три дня своей жизни, отслеживая очень странную ошибку, когда unordered_map :: insert () уничтожает вставленную вами переменную. Это крайне неочевидное поведение наблюдается только в самых последних компиляторах: я обнаружил, что clang 3.2–3.4 и GCC 4.8 - единственные компиляторы, демонстрирующие эту «особенность».
Вот небольшой сокращенный код из моей основной базы кода, который демонстрирует проблему:
#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;
}
Я, как, вероятно, большинство программистов на C ++, ожидал, что вывод будет выглядеть примерно так:
a.second is 0x8c14048
a.second is now 0x8c14048
Но с clang 3.2-3.4 и GCC 4.8 вместо этого я получаю следующее:
a.second is 0xe03088
a.second is now 0
Это может не иметь смысла, пока вы внимательно не изучите документы для unordered_map :: insert () на http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/, где перегрузка № 2:
template <class P> pair<iterator,bool> insert ( P&& val );
Это жадная перегрузка перегрузки универсальной ссылки, которая потребляет все, что не соответствует ни одной из других перегрузок, и перемещает построение в value_type. Итак, почему наш код выше выбрал именно эту перегрузку, а не перегрузку unordered_map :: value_type, как, вероятно, большинство ожидает?
Ответ пристально смотрит вам в лицо: unordered_map :: value_type - это пара < const int, std :: shared_ptr>, и компилятор будет правильно думать, что пара < int , std :: shared_ptr> неконвертируема. Поэтому компилятор выбирает перегрузку универсальной ссылки move, которая уничтожает оригинал, несмотря на то, что программист не использует std :: move (), что является типичным соглашением для указания того, что вы в порядке с уничтожением переменной. Поэтому поведение разрушения вставки на самом деле правильное согласно стандарту C ++ 11, а старые компиляторы были неправильными .
Теперь вы, наверное, понимаете, почему мне потребовалось три дня, чтобы диагностировать эту ошибку. Это было совсем не очевидно в большой кодовой базе, где тип, вставляемый в unordered_map, был typedef, определенным далеко в терминах исходного кода, и никому не приходило в голову проверить, идентично ли typedef значению value_type.
Итак, мои вопросы к Stack Overflow:
Почему старые компиляторы не уничтожают переменные, вставленные как новые компиляторы? Я имею в виду, что даже GCC 4.7 этого не делает, и он довольно соответствует стандартам.
Широко ли известна эта проблема, ведь при обновлении компиляторов код, который раньше работал, внезапно перестанет работать?
Намерял ли комитет по стандартам C ++ такое поведение?
Как бы вы посоветовали изменить unordered_map :: insert () для улучшения поведения? Я спрашиваю об этом, потому что, если здесь есть поддержка, я намерен отправить это поведение в качестве примечания N в WG21 и попросить их реализовать лучшее поведение.
a
- нет. Он должен сделать копию. Кроме того, это поведение полностью зависит от stdlib, а не от компилятора.4.9.0 20131223 (experimental)
соответственно. Выходa.second is now 0x2074088
для меня (или аналогичный).Ответы:
Как отмечали другие в комментариях, «универсальный» конструктор на самом деле не должен всегда отклоняться от своего аргумента. Он должен перемещаться, если аргумент действительно является rvalue, и копировать, если это lvalue.
Вы видите, что поведение, которое всегда меняется, является ошибкой в libstdc ++, которая теперь исправлена в соответствии с комментарием к вопросу. Для тех, кому интересно, я взглянул на заголовки g ++ - 4.8.
bits/stl_map.h
, строки 598-603bits/unordered_map.h
, линии 365-370Последний неправильно использует то,
std::move
где он должен использоватьсяstd::forward
.источник
libstdc++-v3/include/bits/
. Я не вижу того же. Понятно{ return _M_h.insert(std::forward<_Pair>(__x)); }
. Для 4.8 могло быть иначе, но пока не проверял.Это то, что некоторые люди называют универсальной ссылкой , но на самом деле это сокращение ссылок . В вашем случае, когда аргумент является lvalue типа,
pair<int,shared_ptr<int>>
он не приведет к тому, что аргумент будет ссылкой на rvalue, и он не должен перемещаться от него.Потому что вы, как и многие другие люди, неверно истолковали содержимое
value_type
контейнера.value_type
Из*map
(будь то упорядоченных или неупорядоченных) являетсяpair<const K, T>
, что в вашем случаеpair<const int, shared_ptr<int>>
. Несоответствующий тип устраняет ожидаемую перегрузку:источник
std::move
что это вообще ничего не меняет.std::forward
эту настройку, чтобы сделать реальную работу ... Скотт Мейерс проделал хорошую работу, установив довольно простые правила пересылки (использование универсальных ссылок).&&
; сворачивание ссылок происходит, когда компилятор создает экземпляр шаблона. Сворачивание ссылок - это причина, по которой универсальные ссылки работают, но моему мозгу не нравится помещать два термина в одну и ту же область.