Предполагается ли Комитету по стандартам С++, что в С++ 11 unordered_map уничтожает то, что он вставляет?
Решено: Это ошибка в libstdС++ < v4.8.2, который GCC v4.8 и clang >= v3.2 будет использоваться, если он присутствует в системе. См. http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57619 для отчета. Спасибо Кейси и Брайану за то, что он дал правильный ответ. Найл
Оригинальный вопрос:
Я только что потерял три дня своей жизни, отслеживая очень странную ошибку, где 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;
}
Я, как и большинство программистов на С++, ожидал, что вывод будет выглядеть примерно так:
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/, где перегрузка no 2:
template <class P> pair<iterator,bool> insert ( P&& val );
Что такое жадная универсальная перегрузка ссылок, потребляющая все, что не соответствует какой-либо другой перегрузке, и переместите ее в value_type. Итак, почему наш код выше выбирает эту перегрузку, а не перегрузку unordered_map:: value_type, как ожидается, большинство из них ожидали бы?
Ответ смотрит вам в лицо: unordered_map:: value_type - это пара и lt; const int, std:: shared_ptr > , и компилятор правильно подумает, что пара < int, std:: shared_ptr > не является конвертируемой. Поэтому компилятор выбирает универсальную ссылочную перегрузку, и это разрушает исходный , несмотря на программист, не использующий std:: move(), который является типичным соглашением для указания, что вы в порядке с уничтоженной переменной. Поэтому поведение разрушения вставки на самом деле правильное в соответствии со стандартом С++ 11, а старые компиляторы были неверными.
Вероятно, теперь вы можете увидеть, почему я потратил три дня на диагностику этой ошибки. Это не было совершенно очевидно в большой базе кода, где тип, который был вставлен в unordered_map, был typedef, определенным далеко в терминах исходного кода, и никому не приходило в голову проверить, идентичен ли typedef значению__type.
Итак, мои вопросы для:
-
Почему старые компиляторы не уничтожают переменные, вставленные как более новые компиляторы? Я имею в виду, что даже GCC 4.7 этого не делает, и это соответствует стандартным стандартам.
-
Является ли эта проблема широко известной, поскольку, безусловно, обновление компиляторов приведет к тому, что код, который раньше работал, внезапно прекратил работу?
-
Был ли комитет по стандартам С++ намереваться это поведение?
-
Как вы предлагаете изменить unordered_map:: insert(), чтобы улучшить поведение? Я спрашиваю об этом, потому что, если есть поддержка здесь, я намерен представить это поведение как примечание N к РГ21 и попросить их реализовать лучшее поведение.
Ответы
Ответ 1
Как отмечали другие комментарии, "универсальный" конструктор фактически не должен всегда переходить от аргумента. Он должен двигаться, если аргумент действительно является rvalue, и копировать, если это lvalue.
Поведение, которое вы наблюдаете, которое всегда перемещается, является ошибкой в libstdС++, которая теперь исправлена в соответствии с комментарием по вопросу. Для любопытных я взглянул на заголовки g++ - 4.8.
bits/stl_map.h
, строки 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
, строки 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)); }
Последнее неверно использует std::move
, где он должен использовать std::forward
.
Ответ 2
template <class P> pair<iterator,bool> insert ( P&& val );
Что такое жадная универсальная перегрузка ссылок, потребляющая все, что не соответствует какой-либо другой перегрузке, и переместите ее в value_type.
Это то, что некоторые люди называют универсальной ссылкой, но на самом деле является рутинной ссылкой. В вашем случае, когда аргумент является lvalue типа pair<int,shared_ptr<int>>
, он будет не приводить к тому, что аргумент является ссылкой rvalue, и он не должен двигаться от него.
Итак, почему наш код выше выбирает эту перегрузку, а не unordered_map:: value_type перегрузка, как ожидалось бы большинство из них?
Потому что вы, как и многие другие люди раньше, неверно истолковали value_type
в контейнере. value_type
of *map
(будь то упорядоченный или неупорядоченный) равен pair<const K, T>
, что в вашем случае pair<const int, shared_ptr<int>>
. Соответствующий тип исключает перегрузку, которую вы ожидаете:
iterator insert(const_iterator hint, const value_type& obj);