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

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

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 standard раздел ассоциативных контейнеров (23.2.4) о emplace(args) (Вставляет объект value_type t, построенный с помощью std::forward<Args>(args)) и insert(t) только упоминает, что значение вставлено или установлено...

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

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


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

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

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

Ответ 1

Ни слова о перемещении (или нет)

Это как раз проблема, она оставила неуказанной, при каких условиях перемещается 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 выходит за рамки.

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

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

Здесь value_type для случая a 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() при вызове с ключом, который оказывается отсутствующим.