Предполагается ли комитетом по стандартам C ++, что в C ++ 11 unordered_map уничтожает то, что вставляет?

114

Я только что потерял три дня своей жизни, отслеживая очень странную ошибку, когда 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:

  1. Почему старые компиляторы не уничтожают переменные, вставленные как новые компиляторы? Я имею в виду, что даже GCC 4.7 этого не делает, и он довольно соответствует стандартам.

  2. Широко ли известна эта проблема, ведь при обновлении компиляторов код, который раньше работал, внезапно перестанет работать?

  3. Намерял ли комитет по стандартам C ++ такое поведение?

  4. Как бы вы посоветовали изменить unordered_map :: insert () для улучшения поведения? Я спрашиваю об этом, потому что, если здесь есть поддержка, я намерен отправить это поведение в качестве примечания N в WG21 и попросить их реализовать лучшее поведение.

Найл Дуглас
источник
10
Тот факт, что он использует универсальную ссылку, не означает, что вставленное значение всегда перемещается - он должен делать это только для rvalue, а простой a- нет. Он должен сделать копию. Кроме того, это поведение полностью зависит от stdlib, а не от компилятора.
Xeo 03
10
Это похоже на ошибку в реализации библиотеки
Дэвид Родригес - dribeas
4
«Таким образом, поведение при уничтожении вставки на самом деле правильное в соответствии со стандартом C ++ 11, а старые компиляторы были неправильными». Извините, но вы ошибаетесь. Из какой части стандарта C ++ вы взяли эту идею? Кстати, cplusplus.com не является официальным.
Ben Voigt
1
Я не могу воспроизвести это в своей системе, и я использую gcc 4.8.2 и 4.9.0 20131223 (experimental)соответственно. Выход a.second is now 0x2074088 для меня (или аналогичный).
47
Это была ошибка GCC 57619 , регресс из серии 4.8, которая была исправлена ​​в версии 4.8.2 в 2013-06.
Кейси

Ответы:

83

Как отмечали другие в комментариях, «универсальный» конструктор на самом деле не должен всегда отклоняться от своего аргумента. Он должен перемещаться, если аргумент действительно является rvalue, и копировать, если это lvalue.

Вы видите, что поведение, которое всегда меняется, является ошибкой в ​​libstdc ++, которая теперь исправлена ​​в соответствии с комментарием к вопросу. Для тех, кому интересно, я взглянул на заголовки 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.

Брайан
источник
11
Clang по умолчанию использует libstdc ++, GCC stdlib.
Xeo
Я использую gcc 4.9 и ищу libstdc++-v3/include/bits/. Я не вижу того же. Понятно { return _M_h.insert(std::forward<_Pair>(__x)); }. Для 4.8 могло быть иначе, но пока не проверял.
Да, думаю, они исправили ошибку.
Брайан
@Brian Нет, я только что проверил свои системные заголовки. То же самое. 4.8.2 кстати.
У меня 4.8.1, так что я думаю, что это было исправлено между двумя.
Брайан
20
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Из *map(будь то упорядоченных или неупорядоченных) является pair<const K, T>, что в вашем случае pair<const int, shared_ptr<int>>. Несоответствующий тип устраняет ожидаемую перегрузку:

iterator       insert(const_iterator hint, const value_type& obj);
Давид Родригес - дрибеас
источник
Я до сих пор не понимаю, почему существует эта новая лемма, «универсальная ссылка», на самом деле не означает ничего конкретного и не имеет хорошего названия для вещи, которая на практике не является «универсальной». Намного лучше запомнить некоторые старые правила сворачивания, которые являются частью поведения метаязыка C ++, как это было с тех пор, как в язык были введены шаблоны, а также новую подпись из стандарта C ++ 11. В любом случае, какая польза от разговоров об этих универсальных ссылках? Уже есть имена и достаточно странные вещи, вроде того, std::moveчто это вообще ничего не меняет.
user2485710 03
2
@ user2485710 Иногда незнание - это блаженство, и наличие зонтичного термина «универсальная ссылка» для всех настроек сворачивания ссылок и вывода типа шаблона, что ИМХО довольно неинтуитивно, плюс требование использовать std::forwardэту настройку, чтобы сделать реальную работу ... Скотт Мейерс проделал хорошую работу, установив довольно простые правила пересылки (использование универсальных ссылок).
Марк Гарсия
1
На мой взгляд, «универсальная ссылка» - это шаблон для объявления параметров шаблона функции, которые могут связываться как с lvalue, так и с rvalue. «Сворачивание ссылок» - это то, что происходит при замене (выводимых или заданных) параметров шаблона в определения, в ситуациях «универсальной ссылки» и других контекстах.
aschepler 03
2
@aschepler: универсальная ссылка - это просто причудливое имя для подмножества сворачиваемых ссылок. Я согласен с тем, что незнание - это блаженство , а также с тем фактом, что наличие причудливого имени делает разговор об этом более легким и модным, и это может способствовать распространению такого поведения. При этом я не большой поклонник этого имени, так как оно отодвигает настоящие правила в угол, где их не нужно знать ... то есть до тех пор, пока вы не попадете в случай, выходящий за рамки того, что описал Скотт Мейерс.
Дэвид Родригес - dribeas
Сейчас бессмысленная семантика, но различие, которое я пытался предложить: универсальная ссылка происходит, когда я проектирую и объявляю параметр функции как параметр-выводимый-шаблон &&; сворачивание ссылок происходит, когда компилятор создает экземпляр шаблона. Сворачивание ссылок - это причина, по которой универсальные ссылки работают, но моему мозгу не нравится помещать два термина в одну и ту же область.
aschepler 03