std::map emplace/insert перемещаемое значение вставляется

В настоящее время я читаю документы C++1y, а пока я пытаюсь понять статью n3873 под названием Улучшенный интерфейс вставки карт с уникальным ключом. В документе говорится, что есть проблема с методами вставки и вставки, она иллюстрирует проблему на следующем примере:

std::map<std::string, std::unique_ptr<Foo>> m;
m["foo"];

std::unique_ptr<Foo> p(new Foo);
auto res = m.emplace("foo", std::move(p));

И после приведенного выше кода он выражает следующее:

Каково значение p? В настоящее время не указано, был ли перемещен p. (Ответ заключается в том, что это зависит от реализации библиотеки.)

Что ж, у меня возникли проблемы при поиске объяснения предыдущей цитаты, главным образом потому, что я не могу найти, где в стандарте указано, что в коде, подобном приведенному выше, перемещать или не перемещать p определяется реализацией; просмотрев раздел ассоциативных контейнеров n3690 (23.2.4 ) о методах emplace(args) (вставляет объект value_type t, созданный с помощью std::forward<Args>(args)) и insert(t) только упоминает, что значение вставляется или размещается...

... тогда и только тогда, когда в контейнере нет элемента с ключом, эквивалентным ключу t.

Ни слова о перемещении (или нет) значения t; с другой стороны, управляемая память p все равно освобождается (если она перемещается, p освобождается после запрета вставки, а если не перемещается, освобождается в конце области), не так ли?


После вступления позвольте мне задать следующие вопросы:

  • Почему перемещение значения при его вставке/размещении в ассоциативный контейнер, в котором уже есть вставленный ключ, устанавливает значение в неопределенное состояние?
  • Где сказано, что эта операция определяется реализацией?
  • Что происходит с p примера? Он действительно свободен?

Пожалуйста, постарайтесь простить, если вопрос выглядит глупо или с очевидным ответом, это может быть связано с моим недостатком понимания английского языка или с тем, что я не привык погружаться в стандартные документы. Любое руководство будет оценено.


person PaperBirdMaster    schedule 11.06.2014    source источник


Ответы (4)


Ни слова о переезде (или нет)

Именно в этом и проблема, не уточняется, при каких условиях mapped_type будет перемещен.

Почему перемещение значения при его вставке/размещении в ассоциативный контейнер, в котором уже есть вставленный ключ, устанавливает значение в неопределенное состояние?

Ничто не мешает реализации сначала переместить unique_ptr во временную переменную, а затем искать ключ "foo". В этом случае, независимо от того, содержит ли уже map ключ или нет, p == nullptr при вызове emplace возвращается.

И наоборот, реализация может условно перемещаться в зависимости от того, существует ключ или нет. Затем, если ключ существует, p != nullptr при возврате вызова функции. Оба метода одинаково правильны, и в первом случае невозможно получить исходное содержимое p, даже если вставка никогда не произойдет, оно будет уничтожено к моменту возврата emplace.

Предлагаемые функции emplace_stable() и emplace_or_update() должны сделать поведение предсказуемым при любых обстоятельствах.

Где сказано, что эта операция определяется реализацией?

Он не указан как определенная реализация, он недоопределен, допуская слишком большую свободу реализации, что может привести к поведению, которое не всегда желательно.

Что происходит с p примера? Он действительно свободен?

В показанном вами примере содержимое p не будет вставлено в карту (поскольку ключ "foo" уже существует). Но p может или не может быть перемещен, когда вызов emplace возвращается.

Утечек ресурсов в любом случае не будет. Если реализация безоговорочно переместит p, она переместит его в локальную копию, которая будет либо уничтожена, если ключ существует, либо вставлена ​​в карту, если ключ не существует.

С другой стороны, если реализация условно перемещает p, она либо будет вставлена ​​в map, либо p будет владеть ею, когда emplace вернется. В последнем случае он, конечно, будет уничтожен, когда p выйдет за рамки.

person Praetorian    schedule 11.06.2014
comment
Я люблю, когда на все мои вопросы есть ответы - person PaperBirdMaster; 11.06.2014
comment
w/r/t Что происходит с p в примере? p не будет вставлен, потому что m["foo"]; уже создал value_type с ключом "foo", но вы еще не знаете, будет ли перемещен p от или нет. - person bcrist; 11.06.2014
comment
@bcrist Спасибо, я случайно пропустил эту часть вопроса. Обновил ответ. - person Praetorian; 11.06.2014

Семантика перемещения в С++ не связана с методами emplace/insert. Последние являются лишь одним из случаев, когда для повышения производительности используется семантика перемещения.

Вы должны узнать о ссылках rvalue и семантике перемещения, чтобы понять, почему p имеет неопределенное значение после строки "m.emplace("foo", std::move(p));"

Вы можете подробно прочитать, например, здесь: http://www.slideshare.net/oliora/hot-c11-1-rvalue-references-and-move-semantics

Короче говоря, оператор std::move(p) сообщает компилятору, что вам больше не нужно содержимое p, и совершенно нормально, что оно будет перемещено куда-то еще. На практике std::move(p) преобразует p в ссылочный тип rvalue (T&&). rvalue существовало в С++ до С++ 11, не имея «официального» типа. Например, выражение (string("foo") + string("bar")) создает значение rvalue, которое представляет собой строку с выделенным буфером, содержащим "foobar". До С++ 11 вы не могли использовать тот факт, что это выражение является полностью временным и исчезнет через секунду (кроме оптимизаций компилятора). Теперь вы получаете это как часть языка:

v.emplace_back(string("foo") + string("bar"))

возьмет временную строку и переместит ее содержимое прямо в контейнер (без лишних выделений).

Он элегантно работает с временными выражениями, но вы не можете сделать это напрямую с переменными (которые противоположны rvalue). Однако в некоторых случаях вы знаете, что эта переменная вам больше не нужна, и вы хотите переместить ее куда-нибудь еще. Для этого вы используете std::move(..), который сообщает компилятору обработать эту переменную как rvalue. Нужно понимать, что использовать его потом нельзя. Это контракт между вами и компилятором.

person Roman    schedule 11.06.2014

Я думаю, что здесь применим третий пункт 17.6.4.9/1 [res.on.arguments] (цитируя N3936):

Каждое из следующего применимо ко всем аргументам функций, определенных в стандартной библиотеке C++, если явно не указано иное.

  • Если аргумент функции имеет недопустимое значение (например, значение вне домена функции или указатель, недопустимый для предполагаемого использования), поведение не определено.
  • Если аргумент функции описывается как массив, указатель, фактически переданный функции, должен иметь такое значение, чтобы все вычисления адреса и доступ к объектам (это было бы допустимо, если бы указатель действительно указывал на первый элемент такого массива) на самом деле действительны.
  • Если аргумент функции связывается с параметром ссылки rvalue, реализация может предположить, что этот параметр является уникальной ссылкой на этот аргумент. [Примечание. Если параметр является универсальным параметром формы T&& и привязано lvalue типа A, аргумент привязывается к ссылке lvalue (14.8.2.1) и, таким образом, не охватывается предыдущим предложением. —конец примечания ] [Примечание. Если программа приводит lvalue к xvalue при передаче этого lvalue библиотечной функции (например, вызывая функцию с аргументом move(x)), программа фактически просит эту функцию рассматривать это lvalue как временное . Реализация может оптимизировать проверки псевдонимов, которые могут потребоваться, если аргумент был lvalue. -конец примечания]

Передавая выражение rvalue, ссылающееся на объект, в ссылочный параметр, вы, по сути, даете стандартной библиотеке разрешение делать с этим объектом все, что ей заблагорассудится. Он может перемещаться из объекта, или нет, или модифицировать его любым другим способом, удобным для реализации стандартной библиотеки.

person Casey    schedule 11.06.2014

Вопреки тому, что говорится в связанной статье, я бы сказал, что язык стандарта почти гарантирует, что этот код делает неправильную вещь: он перемещает указатель из p, а затем уничтожает объект, на который изначально указывает p, потому что в конце ничего не вставляется в карту m (поскольку ключ, построенный из "foo", уже присутствует). [Я говорю «почти» только потому, что язык Стандарта менее ясен, чем хотелось бы; очевидно, что данный вопрос просто не приходил в голову тому, кто это написал.]

Ссылаясь на таблицу 102 в 23.2.4, запись a_uniq.emplace(args), эффект

Вставляет объект value_type t, созданный с помощью std::forward<Args>(args)...if и только в том случае, если в контейнере нет элемента с ключом, эквивалентным ключу t.

Здесь value_type для случая std::map равно std::pair<const Key, T>, в примере с Key равным std::string и T равным std::unique_ptr<Foo>. Таким образом, объект t, о котором идет речь, конструируется (или будет) как

std::pair<const std::string, std::unique_ptr<Foo>> t("foo", std::move(p));

а «ключ t» является первым компонентом этой пары. Как указано в связанной статье, язык неточен из-за смешения «конструкции» и «вставки»: можно было бы истолковать, что «если и только если» относится к ним обоим, и поэтому t ни построено, ни вставляется, если в контейнере есть элемент с ключом, эквивалентным ключу t; тогда в этом сценарии ничего не будет перемещено из p (из-за отсутствия конструкции) и p не станет нулевым. Однако в таком прочтении цитируемой фразы есть логическая непоследовательность: если t никогда не следует строить, к чему, черт возьми, может относиться «ключ t»? Поэтому я думаю, что единственно разумное прочтение этого текста таково: объект t (безоговорочно) конструируется, как указано, а затем t вставляется в контейнер тогда и только тогда, когда в контейнере нет элемента с ключом, эквивалентным ключу t . В случае, если t не вставлен (как в примере), временное исчезнет при возврате из обращения к emplace, уничтожая перемещенный в него ресурс по ходу.

Конечно, это не означает, что реализация не может поступать правильно: отдельно создавать первый (ключевой) компонент t, искать этот ключ в контейнере и только если он не найден, создайте полную пару t (в это время переместив сопоставленную объектную форму p во второй компонент t) и вставив ее. (Для этого требуется, чтобы тип ключа был копируемым или перемещаемым, так как то, что станет первым компонентом t, изначально создается в другом месте.) Именно потому, что такая реализация возможна, в статье предлагается предоставить средства для надежного просить о таком поведении. Но текущий язык стандарта, похоже, не дает лицензии на такую ​​реализацию и тем более не обязывает вести себя подобным образом.


Позвольте мне добавить, что я столкнулся с этой проблемой на практике, потому что я наивно полагал, что, имея хороший новый метод emplace, он наверняка будет хорошо работать с семантикой перемещения. Итак, я написал что-то вроде строк:

auto p = m.emplace(key,std::move(mapped_to_value));
if (not p.second) // no insertion took place
{ /* some action with value p.first->second about to be overwritten here */
  p.first->second = std::move(mapped_to_value) // replace mapped-to value
}

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

auto range = m.equal_range(key);
if (range.first==range.second) // the key was previously absent; insert a pair
  m.emplace_hint(range.first,key,std::move(mapped_to_value));
else // the key was present, replace the associated value
{ /* some action with value range.first->second about to be overwritten here */
  range.first->second = std::move(mapped_to_value) // replace mapped-to value
}

Это разумный обходной путь, который работает без особых предположений о типе сопоставления (в частности, он не должен быть конструируемым по умолчанию или копируемым, просто конструируемым с перемещением и назначаемым с помощью перемещения).

Похоже, эта идиома должна работать даже для unordered_map, хотя я не пробовал ее для этого случая. На самом деле, приглядевшись, он работает, но использование emplace_hint бессмысленно, так как в отличие от случая std::map метод std::unordered_map::equal_range обязан при отсутствии ключа возвращать пару итераторов, оба равные к (неинформативному) значению, возвращаемому std::unordered_map::end, а не к какой-либо другой паре равных итераторов. Действительно, кажется, что std::unordered_map::emplace_hint, которому разрешено игнорировать подсказку, почти вынужден это сделать, так как либо ключ уже присутствует и emplace_hint не должен ничего делать (кроме как сожрать ресурсы, возможно перемещенные в его временную пару t), либо (такого ключа нет) нет никакого способа получить полезную подсказку, так как ни методы m.find, ни m.equal_range не могут возвращать ничего, кроме m.end() при вызове с отсутствующим ключом.

person Marc van Leeuwen    schedule 17.07.2014