Предполагается ли Комитету по стандартам С++, что в С++ 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);