stl map operator [] плохо?

Мои обозреватели кода указали на то, что использование оператора [] на карте очень плохое и приводит к ошибкам:

map[i] = new someClass;    // potential dangling pointer when executed twice

Или

if (map[i]==NULL) ...      // implicitly create the entry i in the map 

Хотя я понимаю риск после прочтения API, что insert() лучше, потому что он проверяет наличие дубликата, таким образом, можно избежать обвинчивания указателя, я не понимаю, что если правильно обрабатывать, то почему [] не может использоваться в все?

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

Надеюсь, кто-то может либо поспорить со мной, либо встать на мою сторону :)

Ответ 1

Единственный раз, о котором я могу думать, где operator[] может быть полезным, - это когда вы хотите установить значение ключа (перезапишите его, если оно уже имеет значение), и вы знаете, что его можно перезаписать (что это должно быть, так как вы должны использовать интеллектуальные указатели, а не необработанные указатели) и является дешевой по умолчанию, а в некоторых контекста значение должно иметь конструкцию и назначение без броска.

например (аналогично вашему первому примеру)

std::map<int, std::unique_ptr<int>> m;
m[3] = std::unique_ptr<int>(new int(5));
m[3] = std::unique_ptr<int>(new int(3)); // No, it should be 3.

В противном случае существует несколько способов сделать это в зависимости от контекста, однако я бы рекомендовал всегда использовать общее решение (таким образом, вы не можете ошибиться).

Найдите значение и создайте его, если он не существует:

1. Общее решение (рекомендуется, как всегда работает)

std::map<int, std::unique_ptr<int>> m;
auto it = m.lower_bound(3);
if(it == std::end(m) || m.key_comp()(3, it->first))
   it = m.insert(it, std::make_pair(3, std::unique_ptr<int>(new int(3)));

2. С дешевой стоимостью по умолчанию

std::map<int, std::unique_ptr<int>> m;
auto& obj = m[3]; // value is default constructed if it doesn't exists.
if(!obj)
{
   try
   {
      obj = std::unique_ptr<int>(new int(3)); // default constructed value is overwritten.
   }
   catch(...)
   {
      m.erase(3);
      throw;
   }
}

3. С дешевой конструкцией по умолчанию и добавлением ценности без броска

std::map<int, my_objecct> m;
auto& obj = m[3]; // value is default constructed if it doesn't exists.
if(!obj)
   obj = my_objecct(3);

Примечание. Вы можете легко обернуть общее решение в вспомогательный метод:

template<typename T, typename F>
typename T::iterator find_or_create(T& m, const typename T::key_type& key, const F& factory)
{
    auto it = m.lower_bound(key);
    if(it == std::end(m) || m.key_comp()(key, it->first))
       it = m.insert(it, std::make_pair(key, factory()));
    return it;
}

int main()
{
   std::map<int, std::unique_ptr<int>> m;
   auto it = find_or_create(m, 3, []
   {
        return std::unique_ptr<int>(new int(3));
   });
   return 0;
}

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

Ответ 2

Вы правы, что map::operator[] следует использовать с осторожностью, но это может быть весьма полезно: если вы хотите найти элемент на карте, а если нет, создайте его:

someClass *&obj = map[x];
if (!obj)
    obj = new someClass;
obj->doThings();

И на карте есть только один поиск. Если new не удается, вы можете, конечно, удалить указатель NULL с карты:

someClass *&obj = map[x];
if (!obj)
    try
    {
        obj = new someClass;
    }
    catch (...)
    {
        obj.erase(x);
        throw;
    }
obj->doThings();

Естественно, если вы хотите что-то найти, но не вставлять его:

std::map<int, someClass*>::iterator it = map.find(x); //or ::const_iterator
if (it != map.end())
{
    someClass *obj = it->second;
    obj->doThings();
}

Ответ 3

Претензии типа "использование оператора [] карты очень плохо" всегда должны быть предупреждающим знаком почти религиозных убеждений. Но, как и в большинстве таких утверждений, где-то есть какая-то правда. Правда здесь, как и в любой другой конструкции в стандартной библиотеке C++: будьте осторожны и знаете, что делаете. Вы можете (случайно) злоупотреблять почти всем.

Одной из распространенных проблем является потенциальная утечка памяти (если ваша карта принадлежит объектам):

std::map<int,T*> m;
m[3] = new T;
...
m[3] = new T;

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

std::map<int,T*> m;
minsert(std::make_pair(3,new T));
...
m.insert(std::make_pair(3,new T));

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

std::map<int,T*> m;
m.insert(std::make_pair(3,new T));
....
T* tmp = new T;
if( !m.insert(std::make_pair(3,tmp)) )
{
    delete tmp;
}

Но это тоже уродливо. Я лично предпочитаю такие простые случаи:

std::map<int,T*> m;

T*& tp = m[3];
if( !tp )
{
    tp = new T;
}

Но это, возможно, такое же количество личных предпочтений, что и у ваших рецензентов кода, которые не позволяют использовать op []...

Ответ 4

  1. operator [] исключается для вставки, потому что по той же причине вы упомянули в своем вопросе. Он не проверяет дубликат ключа и перезаписывает его на существующий.
  2. operator [] в основном избегают поиска на std::map. Потому что, если ключ не существует на вашей map, тогда operator [] будет молча создавать новый ключ и инициализировать его (как правило, до 0). Который не может быть предпочтительным во всех случаях. Следует использовать [] только в том случае, если необходимо создать ключ, если он не существует.

Ответ 5

Это совсем не проблема с []. Это проблема с хранением исходных указателей в контейнерах.

Ответ 6

Если ваша карта похожа, например, на следующее:

std::map< int, int* >

то вы проиграете, потому что следующий фрагмент кода будет утечка памяти:

std::map< int, int* > m;
m[3] = new int( 5 );
m[3] = new int( 2 );

если правильно обрабатывать, почему [] нельзя использовать вообще?

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

Кроме того, если использовать правильно, нет ничего плохого в использовании map::operator[]. Однако, вероятно, вам будет лучше использовать методы insert/find из-за возможной модификации безмолвной карты.

Ответ 7

 map[i] = new someClass;    // potential dangling pointer when executed twice

здесь проблема не является operator[] карты operator[], а * отсутствием интеллектуальных указателей. Ваш указатель должен быть сохранен в некоторый объект RAII (например, интеллектуальный указатель), который немедленно переходит в собственность выделенного объекта и гарантирует, что он будет освобожден.

Если ваши обозреватели кода проигнорируют это, и вместо этого скажу, что вам следует запросить operator[], купите им хороший учебник C++.

if (map[i]==NULL) ...      // implicitly create the entry i in the map 

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

Ответ 8

Вообще проблема заключается в том, что оператор [] неявно создает значение, связанное с переданным ключом, и вставляет новую пару на карту, если ключ уже не встречается. После этого вы можете нарушить логику, например, при поиске определенного ключа.

map<int, int> m;
if (m[4] != 0) {
  cout << "The object exists" << endl; //furthermore this is not even correct 0 is totally valid value
} else {
  cout << "The object does not exist" << endl;
}
if (m.find(4) != m.end()) {
  cout << "The object exists" << endl; // We always happen to be in this case because m[4] creates the element
}

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

Ответ 9

Нет ничего плохого в том, что operator[] карты сам по себе, если его семантика соответствует тому, что вы хотите. Проблема заключается в определении того, что вы хотите (и зная точную семантику operator[]). Бывают случаи, когда неявное создание новой записи со значением по умолчанию, когда запись отсутствует, - это именно то, что вы хотите (например, подсчет слов в текстовом документе, где ++ countMap[word] - это все, что вам нужно); есть много других случаев, когда это не так.

Более серьезной проблемой в вашем коде может быть то, что вы храните указатели на карте. Более естественным решением может быть использование map <keyType, someClass>, а не map <keyType, SomeClass*>. Но опять же, это зависит от желаемой семантики; например, я использую много map которые инициализируются один раз, при запуске программы, с указателями на статические экземпляры. Если вы являетесь map[i] =... находится в цикле инициализации, который выполняется один раз при запуске, вероятно, нет проблемы. Если это что-то выполняется во многих разных местах кода, вероятно, проблема.

Решение проблемы заключается не в запрете operator[] (или на карте указателей). Решение должно начинаться с определения точной семантики, в которой вы нуждаетесь. И если std::map не предоставляет их напрямую (это редко бывает), напишите небольшой класс-оболочку, который определяет точную семантику, которую вы хотите, используя std::map для их реализации. Таким образом, ваша оболочка для operator[] может быть:

MappedType MyMap::operator[]( KeyType const& key ) const
{
    MyMap::Impl::const_iterator elem = myImpl.find( key );
    if ( elem == myImpl.end() )
        throw EntryNotFoundError();
    return elem->second;
}

или:

MappedType* MyMap::operator[]( KeyType const& key ) const
{
    MyMap::Impl::const_iterator elem = myImpl.find( key );
    return elem == myImpl.end()
        ?   NULL   //  or the address of some default value
        :   &elem->second;
}

Точно так же вы можете использовать insert а не operator[] если вы действительно хотите вставить значение, которое еще не существует.

И я почти никогда не видел случая, когда вы вставляете сразу new ed-объект в карту. Обычная причиной для использования new и delete является то, что объекты, о которых идет речь есть конкретные жизней своих собственные (и не воспроизводимые-хотя и не абсолютное правило, если вы new ИНГ объект, который поддерживает копирование и назначение, вы возможно, что-то не так). Когда отображаемый тип является указателем, то либо объекты, указывающие на объекты, являются статическими (и карта становится более или менее постоянной после инициализации), либо вставка и удаление выполняются в конструкторе и деструкторе класса. (Но это всего лишь общее правило, есть, конечно, исключения).