Чи призначено комітетом зі стандартів C ++ те, що в C ++ 11 unorряд_map знищує те, що він вставляє?


114

Я щойно втратив три дні свого життя, відслідковуючи дуже дивну помилку, де unordered_map :: insert () знищує змінну, яку ви вставляєте. Ця надзвичайно очевидна поведінка зустрічається лише в останніх компіляторах: я виявив, що кланг 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. То чому ж наш код вище обрав це перевантаження, а не не упорядкований_мап :: value_type перевантаження, як, мабуть, більшість очікував?

Відповідь дивиться вам в обличчя: unordered_map :: value_type є пара < const int, std :: shared_ptr>, і компілятор правильно вважає, що пара < int , std :: shared_ptr> не конвертована. Тому компілятор вибирає універсальну контрольну перевантаження переміщення, і це знищує оригінал, незважаючи на те, що програміст не використовує 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
Тільки тому, що він використовує універсальний коефіцієнт ref, не означає, що вставлене значення завжди переміщується - воно повинно робити це лише коли-небудь для rvalues, чого звичайна aні. Слід зробити копію. Також ця поведінка повністю залежить від stdlib, а не від компілятора.
Xeo

10
Це здається помилкою у впровадженні бібліотеки
David Rodríguez - dribeas

4
"Тому поведінка руйнування вставок насправді правильна відповідно до стандарту C ++ 11, і старі компілятори були невірними." Вибачте, але ви помиляєтесь. З якої частини стандарту C ++ ви взяли цю ідею? BTW cplusplus.com не є офіційним.
Бен Войгт

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

Як зазначали інші в коментарях, "універсальний" конструктор насправді не повинен завжди рухатися від свого аргументу. Він повинен переміститися, якщо аргумент справді є реальним значенням, і скопіювати, якщо це значення.

Поведінка, яку ви спостерігаєте, яка завжди рухається, - це помилка в 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 ++ за замовчуванням, stdlib GCC.
Xeo

Я використовую gcc 4.9 і шукаю libstdc++-v3/include/bits/. Я не бачу того самого. Я бачу { return _M_h.insert(std::forward<_Pair>(__x)); }. Це може бути інакше для 4.8, але я ще не перевірив.

Так, так що я думаю, вони виправили помилку.
Брайан

@Brian Nope, я просто перевірив свої заголовки системи. Однакові речі. 4.8.2 btw.

Міна 4.8.1, тож я думаю, це було зафіксовано між ними.
Брайан

20
template <class P> pair<iterator,bool> insert ( P&& val );

Що є жадібним універсальним контрольним перевантаженням, що споживає все, що не відповідає жодному іншому перевантаженню, і переходить, будуючи його на тип value_type.

Це те, що деякі називають універсальним посиланням , але насправді реферат руйнується . У вашому випадку, коли аргумент є іменують типу pair<int,shared_ptr<int>>це НЕ призведе до аргументу будучи посилання RValue і не повинно рухатися від нього.

То чому ж наш код вище обрав це перевантаження, а не не упорядкований_мап :: 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що взагалі нічого не рухають.
користувач2485710

2
@ user2485710 Іноді невігластво - це блаженство, і те, що паралельний термін "універсальна довідка" для всіх посилань, що згортаються, та виведення виду шаблону, піддається зміні, що IMHO є досить неінтуїтивним, плюс необхідність використовувати std::forwardдля того, щоб цей твік справді працював ... Скотт Мейєрс зробив гарну роботу, встановивши досить прості правила для переадресації (використання універсальних посилань).
Марк Гарсія

1
На мій погляд, "універсальна посилання" - це шаблон для оголошення параметрів шаблону функції, який може прив'язуватися як до значень, так і до rvalues. "Звалення посилань" - це те, що відбувається під час заміни (виведених або заданих) параметрів шаблону на визначення, у ситуаціях "універсальної посилання" та інших контекстах.
aschepler

2
@aschepler: універсальна посилання - це лише фантастична назва для підмножини рефератів, що згортаються. Я погоджуюся з невіглаством - це блаженство , а також той факт, що маючи фантазійне ім’я, про це говорять простіше і модніше, що може допомогти пропагувати поведінку. За словами, я не є великим прихильником цього імені, оскільки воно підштовхує фактичні правила до куточка, де їх не потрібно знати ... тобто до тих пір, поки ви не потрапите на випадок поза тим, що описав Скотт Майєрс.
David Rodríguez - dribeas

Тепер безглузда семантика, але відмінність, яку я намагався запропонувати: Універсальна посилання відбувається, коли я розробляю і оголошую параметр функції як виразний шаблон-параметр &&; згортання посилань відбувається, коли компілятор інстанціює шаблон. Згадування посилань є причиною роботи універсальних посилань, але моєму мозку не подобається ставити два терміни в одну область.
aschepler
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.