Обоснование семантики вставки карты std в C ++?

Меня немного смущает семантика std::map::insert. То есть я не жалуюсь - стандарт - это стандарт, а API - такой, какой он есть. Все еще,

insert будет

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

И - только в версии с одним аргументом pair<iterator,bool> insert ( const value_type& x ); он даже скажет вам, вставил ли он (новое, возможно, другое) значение в ключ (и). Насколько я понимаю, версии итератора будут молча игнорировать вставки, если ключ уже существует.

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

На примере:

Есть несколько основных способов реализовать вставку в одноключевой карте, такой как std::map:

  • вставить, заменить, если уже существует
  • вставить, игнорировать, если уже существует (это поведение std :: map)
  • вставить, выдать ошибку, если она уже существует
  • вставить, UB, если уже существует

Теперь я пытаюсь понять, почему insert_or_ignore имеет больше смысла, чем insert_or_replace (или insert_or_error)!


Я просмотрел свою копию TC ++ PL (к сожалению, у меня есть только немецкая версия), и что интересно, Страуструп пишет в главе 17.4.1.7 (список операций для map): (извините, грубый перевод с немецкого)

(...) Обычно никого не волнует, вставлен ли ключ (sic!) Заново или уже существовал до вызова insert() (...)

Что, как мне кажется, будет справедливо только для set, а не для map, потому что для карты это имеет большое значение, если предоставленное значение было вставлено либо старый остается на карте. (Очевидно, это не имеет значения для ключа, поскольку он эквивалентен.)


Примечание. Я знаю о operator[] и знаю об элементе 24 Эффективный STL и предложенная там efficientAddOrUpdate функция. Мне просто интересно обоснование семантики insert, потому что я лично считаю их противоречащими интуиции.


person Martin Ba    schedule 14.05.2012    source источник
comment
Ну, вы не просили его изменить существующее значение, вы попросили его вставить (новое) значение. Я согласен с тем, что более регулярное сообщение о неудачах было бы хорошо. Вы по-прежнему можете разыменовать возвращенный итератор и проверить, присутствует ли новое значение или старое, или даже использовать этот итератор для обновления существующего значения.   -  person Ben Voigt    schedule 14.05.2012
comment
Если вы хотите заменить / создать, используйте operator[].   -  person BoBTFish    schedule 14.05.2012
comment
Вот код для принудительной вставки на карту.   -  person Kerrek SB    schedule 14.05.2012
comment
@KerrekSB: почему бы не просто использовать идиоматический operator[]?   -  person Matthieu M.    schedule 14.05.2012
comment
@MatthieuM .: Что делать, если вам нужно принять дополнительные решения и / или вести журнал в зависимости от того, существует ли значение уже или нет?   -  person Kerrek SB    schedule 14.05.2012
comment
@Ben - смотри мою правку ... как я понял, нет возможности просто вставить в карту без учета дубликатов. Вы можете только insert_или _... wrt. на дубликаты, поэтому я думаю, что вопрос верен, почему STL выбирает ..._ ignore.   -  person Martin Ba    schedule 14.05.2012
comment
@KerrekSB: тогда вы, очевидно, не можете повторно использовать insert_forcefully, поэтому вы реализуете свое собственное поведение.   -  person Matthieu M.    schedule 14.05.2012
comment
@KerrekSB: insert возвращает итератор, который при желании можно использовать для обновления значения.   -  person Ben Voigt    schedule 14.05.2012
comment
В C ++ 11 при желании можно использовать функцию-член at (третий вариант в примере выше).   -  person Phil1970    schedule 25.09.2018
comment
C ++ 17 добавляет en.cppreference.com/w/cpp/container/map / insert_or_assign :-)   -  person Martin Ba    schedule 21.07.2021


Ответы (5)


Я не знаю официального обоснования, но хотел бы отметить двойственность с operator[].

Кажется очевидным, что хотелось бы двух вариантов вставки:

  • чисто аддитивный
  • аддитивный / деструктивный

Если мы рассматриваем map как разреженное представление массива, то наличие operator[] имеет смысл. Я не знаю, существовали ли ранее существовавшие словари и диктовали ли этот синтаксис (возможно, почему бы и нет).

Кроме того, все контейнеры STL имеют несколько перегрузок insert, и это сходство интерфейсов - то, что позволяет универсальное программирование.

Следовательно, у нас есть как минимум два претендента на API: operator[] и insert.

Теперь, в C ++, если вы прочитаете:

array[4] = 5;

Естественно, что содержимое ячейки с индексом 4 было деструктивно обновлено. Таким образом, вполне естественно, что map::operator[] должен возвращать ссылку, чтобы разрешить это деструктивное обновление.

На этом этапе нам также нужна чисто аддитивная версия, и у нас есть этот insert метод. Почему нет ?

Конечно, можно было задать insert ту же семантику, что и operator[], а затем реализовать insert_or_ignore метод наверху. Хотя это было бы больше работы.

Поэтому, хотя я согласен с тем, что это может быть удивительно, я думаю, что мои рассуждения не слишком ошибочны и могут быть вероятным объяснением обстоятельств, которые привели нас сюда :)


Что касается предложенных вами альтернатив:

  • вставить, UB, если уже существует

К счастью, это не так!

  • вставить, выдать ошибку, если она уже существует

Только Java (и производные от нее) безумны. C ++ был задуман в то время, когда исключения использовались в исключительных обстоятельствах.

  • вставить, заменить, если уже существует
  • вставить, игнорировать, если уже существует (это поведение std :: map)

Мы согласны с тем, что выбор был между одним из них. Обратите внимание, что даже несмотря на то, что map выбрал второй вариант, он не полностью игнорирует тот факт, что элемент уже существует, по крайней мере, в версии с одним элементом, поскольку предупреждает, что элемент не был вставлен.

person Matthieu M.    schedule 14.05.2012
comment
+1, хотя я не уверен, что нам действительно нужна чисто аддитивная функция, поскольку мы всегда можем сначала вызвать find(). (с обоими insert или / или op[]) Но, возможно, двойственность была / является хорошей причиной. - person Martin Ba; 14.05.2012
comment
@Martin: Я согласен с тем, что find работал бы над созданием чисто аддитивной функции. Однако для этого потребовалась бы некоторая стандартная плита. - person Matthieu M.; 14.05.2012
comment
Да, что интересно, аналогичный шаблон (или вспомогательная fn) вам сейчас нужен для эффективного insert_or_replace :-) - person Martin Ba; 14.05.2012
comment
@Martin: нет, вы просто используете map[key] = value, и он вставляет или заменяет. - person Matthieu M.; 14.05.2012
comment
Единственная проблема в том, что map[key] = val неэффективен, потому что он сначала по умолчанию создает элемент, если он не существует. Часто не имеет большого значения, но, тем не менее, раздражает. - person Martin Ba; 14.05.2012
comment
@Martin: и, что еще хуже, проблема в том, что для него требуется конструктор по умолчанию (который в противном случае не требуется для использования map). Ну что ж... - person Matthieu M.; 14.05.2012
comment
Жаль, что в C ++ нет operator[]=(key, value). - person dan04; 15.05.2012

Метод вставки - это просто не то, что вы ищете, это звучит как ... Метод вставки предназначен для выполнения именно того, что подразумевает название ... вставки значений. Я согласен с тем, что возможность создать значение, если его еще нет, и заменить то, которое есть, в некоторых ситуациях важна, но в других вы бы просто предпочли не обрабатывать исключения, возвращаемые значения и т. Д., Если вы просто хотите сделать вставку, только если значение еще не указано.

Похоже, что метод, который вы ищете (как указано выше в BoBTFish), вероятно, является оператором []. Просто используйте это так:

myMap["key"] = "value";

Это пройдёт по вашей карте, найдет ключ «ключ» и заменит соответствующее значение на «значение». Если ключа нет, он его создаст. Оба метода очень полезны в разных ситуациях, и я обнаружил, что использую оба только в зависимости от того, что мне нужно.

person Kreg    schedule 14.05.2012
comment
Я отредактировал твой пост. Во-первых, не подписывайте (явно), это не одобряется (ваш пользователь все равно виден); во-вторых, проверьте синтаксис уценки, чтобы узнать, как форматировать фрагменты встроенного кода и фрагменты многострочного кода. Спасибо за ответ и добро пожаловать на SO :) - person Matthieu M.; 14.05.2012

Я не утверждаю, что знаю исходное обоснование решения, но придумать его несложно. Я думаю ;-)

Текущее поведение «вставить или игнорировать» позволяет очень легко реализовать два других - по крайней мере, для тех из нас, кто не боится создания и использования функций, не являющихся членами, для дополнения функциональности стандартной библиотеки («это не ООП- у достаточно! ").

Пример (написано на месте, поэтому могут быть ошибки):

template<typename Map>
void insert_or_update(Map& map, typename Map::value_type const& x)
{
  std::pair<typename Map::iterator, bool> result = map.insert(x);
  if (!result.second)
    result.first->second = x.second; // or throw an exception (consider using
                                     // a different function name, though)
}

Обратите внимание, что как есть, приведенная выше функция на самом деле не сильно отличается от operator[] - да, она избегает инициализации по умолчанию, но в то же время (потому что я ленив) она не может извлечь выгоду из семантики перемещения, которую вы выполняете. -date STL, вероятно, уже поддерживает operator[].

В любом случае, любое другое поведение вставки для map сделало бы его более утомительным для реализации других, поскольку map::find возвращает только конечный дозорный, если ключ еще не находится на карте. Конечно, с помощью <algorithm> (и особенно lower_bound) можно было бы писать эффективные вспомогательные функции, не утопая их в деталях реализации и уродливых общих конструкциях, таких как циклы ;-).

person eq-    schedule 19.05.2012

Начиная с insert() вы не ожидаете, что будут затронуты существующие объекты в контейнере. Поэтому простое их не трогает.

person ypnos    schedule 14.05.2012
comment
Не отвечает на часть вопроса почему она не сообщает об ошибке?, но да, она отвечает на одну часть вопроса. Поведение функции довольно интуитивно понятно, ожидается insert вызов изменение существующих элементов было бы довольно противоречивым. - person Alok Save; 14.05.2012
comment
Нет смысла оскорблять, но вы повторяете определенную семантику функции, не предоставляя обоснования. Почему я не ожидаю, что существующие элементы будут затронуты? Потому что это называется insert, а не insert_or_replace или что? Опять же, для меня это было нелогично, и это не помогает мне понять, когда мне говорят «нет», это не так. :-) - person Martin Ba; 14.05.2012
comment
@Martin: Разве insert_or_ignore (как в SQLite) не было бы лучше, чем insert_or_replace? - person dan04; 14.05.2012
comment
@ dan04 - да для указанной семантики. (Insert_or_replace - это то, что я лично считаю более интуитивным для вставки и.) - person Martin Ba; 14.05.2012

pair<iterator,bool> ‹- часть bool не говорит об успешной вставке или нет?

Вы можете просто обновить часть значения возвращаемого итератора, если часть bool имеет значение false, чтобы обновить существующий элемент с тем же ключом.

person BlueWanderer    schedule 14.05.2012
comment
В вопросе упоминалось об этом. Вы проигнорировали часть вопроса, в которой упоминались другие четыре перегрузки std::map::insert. - person Ben Voigt; 14.05.2012