В картах STL лучше использовать map :: insert, чем []?

201

Некоторое время назад у меня была дискуссия с коллегой о том, как вставить значения в карты STL . Я предпочел, map[key] = value; потому что это естественно и понятно для чтения, тогда как он предпочитал map.insert(std::make_pair(key, value))

Я просто спросил его, и никто из нас не может вспомнить причину, по которой вставка лучше, но я уверен, что это был не просто стиль, а техническая причина, например, эффективность. Ссылка SGI STL просто говорит : «Строго говоря, эта функция - член не является необходимым: она существует только для удобства.»

Кто-нибудь может сказать мне эту причину, или я просто мечтаю, что она есть?

данио
источник
2
Спасибо за все замечательные ответы - они были действительно полезны. Это отличная демонстрация переполнения стека в лучшем виде. Меня разрывало, какой ответ должен быть принят: netjeff более четко описывает другое поведение, Грег Роджерс упомянул проблемы с производительностью. Хотел бы я отметить оба.
Данио
6
На самом деле, с C ++ 11 вам, вероятно, лучше всего использовать map :: emplace, который избегает двойной конструкции
einpoklum
@einpoklum: На самом деле Скотт Мейерс предлагает иное в своем выступлении «Развивающийся поиск эффективного C ++».
Томас Эдинг
3
@einpoklum: Это тот случай, когда мы используем новую память. Но из-за некоторых требований стандартов к карте существуют технические причины, по которым emplace может быть медленнее, чем вставка. Разговор находится в свободном доступе на YouTube, например , в этой ссылке youtube.com/watch?v=smqT9Io_bKo @ ~ 38-40 мин марки. Для ссылки SO, вот stackoverflow.com/questions/26446352/…
Томас Эдинг
1
Я бы на самом деле поспорил с некоторыми из того, что представил Мейерс, но это выходит за рамки этой цепочки комментариев, и в любом случае, я думаю, мне придется отказаться от своего предыдущего комментария.
einpoklum

Ответы:

240

Когда пишешь

map[key] = value;

нет никакого способа узнать , если вы заменили на valueFOR key, или если вы создали новый keyс value.

map::insert() создаст только:

using std::cout; using std::endl;
typedef std::map<int, std::string> MyMap;
MyMap map;
// ...
std::pair<MyMap::iterator, bool> res = map.insert(MyMap::value_type(key,value));
if ( ! res.second ) {
    cout << "key " <<  key << " already exists "
         << " with value " << (res.first)->second << endl;
} else {
    cout << "created key " << key << " with value " << value << endl;
}

Для большинства моих приложений меня обычно не волнует, создаю я или заменяю, поэтому я использую более легкое для чтения map[key] = value.

netjeff
источник
16
Следует отметить, что map :: insert никогда не заменяет значения. И в общем случае я бы сказал, что лучше использовать, (res.first)->secondа не valueи во втором случае.
Dalle
1
Я обновил, чтобы быть более ясным, что map :: insert никогда не заменяет. Я оставил, elseпотому что я думаю, что использование valueяснее, чем итератор. Только если тип значения имеет необычный экземпляр ctor или op ==, он будет другим, и этот тип вызовет другие проблемы при использовании контейнеров STL, таких как map.
нетжефф
1
map.insert(std::make_pair(key,value))должно быть map.insert(MyMap::value_type(key,value)). Тип, возвращаемый из make_pair, не соответствует типу, принятому в, insertи текущее решение требует преобразования
Дэвид Родригес - dribeas
1
есть способ узнать, вставлен ли вы или только назначен operator[], просто сравните размер до и после. Имхо возможность вызывать map::operator[]только для конструируемых типов по умолчанию гораздо важнее.
idclev 463035818
@ DavidRodríguez-dribeas: Хорошее предложение, я обновил код в своем ответе.
нетжефф
53

Они имеют разную семантику, когда дело доходит до ключа, уже существующего на карте. Так что они на самом деле не сопоставимы напрямую.

Но версия оператора [] требует создания значения по умолчанию, а затем присвоения, поэтому, если это дороже, чем копирование, то это будет дороже. Иногда конструкция по умолчанию не имеет смысла, и тогда было бы невозможно использовать версию operator [].

Грег Роджерс
источник
1
make_pair может потребовать конструктор копирования - это будет хуже, чем по умолчанию. +1 в любом случае.
1
Главное, как вы сказали, что у них разная семантика. Так что ни один не лучше другого, просто используйте тот, который делает то, что вам нужно.
jalf
Почему конструктор копирования будет хуже, чем конструктор по умолчанию, за которым следует присваивание? Если это так, то человек, который написал класс, что-то пропустил, потому что независимо от того, что делает оператор =, он должен был сделать то же самое в конструкторе копирования.
Стив Джессоп
1
Иногда создание по умолчанию так же дорого, как и само присвоение. Естественно, назначение и копирование будут эквивалентны.
Грег Роджерс
@Arkadiy В оптимизированной сборке компилятор часто удаляет много ненужных вызовов конструктора копирования.
antred
35

Еще одна вещь, чтобы отметить с std::map:

myMap[nonExistingKey];создаст новую запись на карте, привязанную к nonExistingKeyинициализированному значению по умолчанию.

Это испугало меня до чертиков, когда я впервые увидел это (пока бился головой об ужасно унаследованную ошибку). Не ожидал этого. Для меня это выглядит как операция get, и я не ожидал «побочного эффекта». Предпочитаю map.find()при получении с вашей карты.

Соколиный Глаз Паркер
источник
3
Это приличный вид, хотя хеш-карты довольно универсальны для этого формата. Это может быть одной из тех «странностей, которые никто не считает странными» только из-за того, насколько широко они используют одни и те же условные обозначения
Стивен Дж
19

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

:)

Torlack
источник
5
Во- вторых! Должен отметить это. Слишком много людей обменивают тупость на ускорение с точностью до секунды. Помилуй нас, бедные души, которые должны поддерживать такие злодеяния!
Мистер Ри
6
Как писал Грег Роджерс: «Эти два имеют разную семантику, когда речь идет о ключе, уже существующем на карте. Таким образом, они на самом деле не сопоставимы».
Dalle
Это старый вопрос и ответ. Но «более читаемая версия» - глупая причина. Потому что то, что наиболее читабельно, зависит от человека.
vallentin
14

insert Лучше с точки зрения безопасности.

Выражение map[key] = valueна самом деле две операции:

  1. map[key] - создание элемента карты со значением по умолчанию.
  2. = value - копирование значения в этот элемент.

Исключение может произойти на втором этапе. В результате операция будет выполнена только частично (новый элемент был добавлен в карту, но этот элемент не был инициализирован сvalue ). Ситуация, когда операция не завершена, но состояние системы изменено, называется операцией с «побочным эффектом».

insertоперация дает надежную гарантию, означает, что она не имеет побочных эффектов ( https://en.wikipedia.org/wiki/Exception_safety ). insertлибо полностью выполнен, либо оставляет карту в неизмененном состоянии.

http://www.cplusplus.com/reference/map/map/insert/ :

Если нужно вставить один элемент, в контейнере нет изменений в случае исключения (строгая гарантия).

anton_rh
источник
1
что более важно, для вставки не требуется, чтобы значение было конструируемым по умолчанию.
UmNyobe
13

Если ваше приложение критично к скорости, я посоветую использовать оператор [], поскольку он создает всего 3 копии исходного объекта, из которых 2 являются временными объектами и рано или поздно уничтожаются как.

Но в insert () создаются 4 копии исходного объекта, из которых 3 являются временными объектами (не обязательно «временными») и уничтожаются.

Что означает дополнительное время для: 1. Выделения памяти для одного объекта 2. Один дополнительный вызов конструктора 3. Один дополнительный вызов деструктора 4. Выделение памяти одного объекта

Если ваши объекты большие, конструкторы типичны, деструкторы много освобождают от ресурсов, а количество очков выше еще больше. Что касается читабельности, я думаю, что оба достаточно справедливы.

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

class Sample
{
    static int _noOfObjects;

    int _objectNo;
public:
    Sample() :
        _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside default constructor of object "<<_objectNo<<std::endl;
    }

    Sample( const Sample& sample) :
    _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside copy constructor of object "<<_objectNo<<std::endl;
    }

    ~Sample()
    {
        std::cout<<"Destroying object "<<_objectNo<<std::endl;
    }
};
int Sample::_noOfObjects = 0;


int main(int argc, char* argv[])
{
    Sample sample;
    std::map<int,Sample> map;

    map.insert( std::make_pair<int,Sample>( 1, sample) );
    //map[1] = sample;
    return 0;
}

Вывод при использовании insert () Вывод, когда используется оператор []

Рампал Чаудхари
источник
4
Теперь запустите этот тест снова с включенной полной оптимизацией.
antred
2
Также рассмотрим, что на самом деле делает оператор []. Сначала он ищет на карте запись, соответствующую указанному ключу. Если он находит его, он перезаписывает значение этой записи указанным. Если это не так, он вставляет новую запись с указанным ключом и значением. Чем больше ваша карта, тем дольше оператор [] будет искать карту. В какой-то момент это будет более чем восполнить дополнительный вызов c'or copy (если он останется в финальной программе после того, как компилятор выполнил свою оптимизацию).
antred
1
@antred, insertдолжен делать тот же поиск, так что нет никакой разницы с этим [](потому что ключи карты уникальны).
СЗ
Хорошая иллюстрация того, что происходит с распечатками - но что на самом деле делают эти 2 бита кода? Зачем нужны 3 копии оригинального объекта?
rbennett485
1. должен реализовать оператор присваивания, иначе вы получите неправильные числа в деструкторе 2. используйте std :: move при построении пары, чтобы избежать избыточного конструирования копии "map.insert (std :: make_pair <int, Sample> (1, std: : move (sample))); "
Fl0
10

Теперь в C ++ 11 я думаю, что лучший способ вставить пару в карту STL:

typedef std::map<int, std::string> MyMap;
MyMap map;

auto& result = map.emplace(3,"Hello");

Результат будет пара с:

  • Первый элемент (result.first), указывает на вставленную пару или указывает на пару с этим ключом, если ключ уже существует.

  • Второй элемент (result.second), true, если вставка была правильной или ложной, что-то пошло не так.

PS: Если у вас нет дела по поводу заказа, вы можете использовать std :: unordered_map;)

Спасибо!

GutiMac
источник
9

Гвоздь с map :: insert () состоит в том, что он не заменит значение, если ключ уже существует на карте. Я видел код C ++, написанный Java-программистами, где они ожидали, что insert () будет вести себя так же, как Map.put () в Java, где значения заменяются.

Энтони Крамп
источник
2

Обратите внимание, что вы также можете использовать Boost.Assign :

using namespace std;
using namespace boost::assign; // bring 'map_list_of()' into scope

void something()
{
    map<int,int> my_map = map_list_of(1,2)(2,3)(3,4)(4,5)(5,6);
}
rlbond
источник
1

Вот еще один пример, показывающий, что значение ключа operator[] перезаписывается, если оно существует, но.insert не перезаписывает значение , если оно существует.

void mapTest()
{
  map<int,float> m;


  for( int i = 0 ; i  <=  2 ; i++ )
  {
    pair<map<int,float>::iterator,bool> result = m.insert( make_pair( 5, (float)i ) ) ;

    if( result.second )
      printf( "%d=>value %f successfully inserted as brand new value\n", result.first->first, result.first->second ) ;
    else
      printf( "! The map already contained %d=>value %f, nothing changed\n", result.first->first, result.first->second ) ;
  }

  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

  /// now watch this.. 
  m[5]=900.f ; //using operator[] OVERWRITES map values
  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

}
bobobobo
источник
1

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

Я видел людей в прошлом, использующих карты в форме

map< const key, const val> Map;

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

const_cast< T >Map[]=val;

Их причина сделать это, насколько я помню, заключалась в том, что они были уверены, что в этих определенных фрагментах кода они не будут перезаписывать значения карты; следовательно, идти вперед с более «читабельным» методом[] .

На самом деле у меня никогда не было прямых проблем с кодом, написанным этими людьми, но до сегодняшнего дня я твердо чувствую, что риски - пусть даже небольшие - не должны приниматься, когда их легко избежать.

В случаях, когда вы имеете дело со значениями карты, которые абсолютно не должны быть перезаписаны, используйте insert. Не делайте исключений просто для удобства чтения.

dk123
источник
Несколько разных людей написали это? Конечно, используйте insert(не input), поскольку это const_castприведет к перезаписи любого предыдущего значения, что очень неконстантно. Или не помечайте тип значения как const. (Подобные вещи обычно являются конечным результатом const_cast, так что это почти всегда красный флаг, указывающий на ошибку где-то еще.)
Potatoswatter
@Potatoswatter Ты прав. Я просто вижу, что const_cast [] используется со значениями константной карты некоторыми людьми, когда они уверены, что не будут заменять старое значение в определенных битах кода; поскольку [] сам по себе более читабелен. Как я уже упоминал в заключительной части моего ответа, я рекомендую использовать insertв случаях, когда вы хотите предотвратить перезапись значений. (Только изменил inputк insert- спасибо)
dk123
@Potatoswatter Если я правильно помню, основными причинами, по которым люди, похоже, пользуются, const_cast<T>(map[key])были: 1. [] более читабелен, 2. они уверены в определенных фрагментах кода, они не будут перезаписывать значения, и 3. они не делают хочу, чтобы другие биты кода ничего не знали, перезаписывая их значения - отсюда и const value.
dk123 27.12.12
2
Я никогда не слышал об этом; где ты это видел? Письмо, const_castкажется, более чем сводит на нет дополнительную «читабельность» [], и такого рода уверенности почти достаточно оснований, чтобы уволить разработчика. Сложные условия работы решаются пуленепробиваемыми конструкциями, а не внутренностями.
Potatoswatter
@Potatoswatter Я помню, это было во время одной из моих прошлых работ по разработке образовательных игр. Я никогда не мог заставить людей писать код изменить свои привычки. Вы абсолютно правы, и я полностью с вами согласен. Из ваших комментариев я решил, что это, вероятно, стоит отметить, чем мой первоначальный ответ, и поэтому я обновил его, чтобы отразить это. Спасибо!
dk123 27.12.12
1

Тот факт, что insert()функция std :: map не перезаписывает значение, связанное с ключом, позволяет нам писать код перечисления объектов следующим образом:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    dict.insert(make_pair(word, dict.size()));
}

Это довольно распространенная проблема, когда нам нужно сопоставить разные неуникальные объекты с некоторыми идентификаторами в диапазоне 0..N. Эти идентификаторы могут быть позже использованы, например, в алгоритмах графа. Альтернатива с operator[]будет выглядеть менее читабельным на мой взгляд:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    size_t sz = dict.size();
    if (!dict.count(word))
        dict[word] = sz; 
} 
mechatroner
источник