Какой предпочтительный/идиоматический способ вставить в карту?

Я определил четыре различных способа вставки элементов в std::map:

std::map<int, int> function;

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

Какой из них является предпочтительным/идиоматическим способом? (И есть ли другой способ, о котором я не думал?)

Ответ 1

Прежде всего, функции-члены operator[] и insert не являются функционально эквивалентными:

  • operator[] будет искать ключ, вставить построенное по умолчанию значение, если оно не найдено, и вернуть ссылку, которой вы присваиваете значение. Очевидно, что это может быть неэффективным, если mapped_type может извлечь выгоду из прямой инициализации, а не по умолчанию, построенной и назначенной. Этот метод также не позволяет определить, действительно ли была введена вставка, или если вы только перезаписали значение для ранее вставленного ключа
  • Функция члена insert не будет иметь никакого эффекта, если ключ уже присутствует на карте и, хотя он часто забывается, возвращает std::pair<iterator, bool>, который может представлять интерес (наиболее важно определить, действительно ли вставка сделано).

Из всех перечисленных возможностей для вызова insert все три почти эквивалентны. Напоминаем, что давайте взглянем на insert подпись в стандарте:

typedef pair<const Key, T> value_type;

  /* ... */

pair<iterator, bool> insert(const value_type& x);

Итак, как разные три вызова?

  • std::make_pair полагается на вывод аргумента шаблона и может (и в этом случае будет) создавать что-то другого типа, чем фактическое value_type на карте, что потребует дополнительного вызова конструктора шаблона std::pair в порядке для преобразования в value_type (то есть: добавление const в first_type)
  • std::pair<int, int> также потребуется дополнительный вызов конструктору шаблона std::pair, чтобы преобразовать параметр в value_type (то есть: добавление const в first_type)
  • std::map<int, int>::value_type не оставляет места сомнению, так как это непосредственно тип параметра, ожидаемый функцией члена insert.

В конце концов, я бы избегал использования operator[], когда цель должна вставляться, если нет дополнительных затрат при построении по умолчанию и назначении mapped_type, и что мне не важно определить, будет ли новый ключ эффективно вставлен. При использовании insert возможно создание пути value_type.

Ответ 2

С С++ 11 у вас есть два основных дополнительных параметра. Во-первых, вы можете использовать insert() с синтаксисом инициализации списка:

function.insert({0, 42});

Это функционально эквивалентно

function.insert(std::map<int, int>::value_type(0, 42));

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

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

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

Во-вторых, вы можете использовать метод emplace():

function.emplace(0, 42);

Это более красноречиво, чем любая из форм insert(), отлично работает с типами только для перемещения, такими как unique_ptr, и теоретически может быть немного более эффективным (хотя достойный компилятор должен оптимизировать разницу). Единственным серьезным недостатком является то, что это может немного удивить ваших читателей, поскольку методы emplace обычно не используются таким образом.

Ответ 3

Первая версия:

function[0] = 42; // version 1

может или не может вставить значение 42 в карту. Если ключ 0 существует, тогда он назначит 42 этому ключу, перезаписав любое значение, которое имеет ключ. В противном случае он вставляет пару ключ/значение.

Функции вставки:

function.insert(std::map<int, int>::value_type(0, 42));  // version 2
function.insert(std::pair<int, int>(0, 42));             // version 3
function.insert(std::make_pair(0, 42));                  // version 4

с другой стороны, ничего не делайте, если ключ 0 уже существует на карте. Если ключ не существует, он вставляет пару ключ/значение.

Три функции вставки почти идентичны. std::map<int, int>::value_type - это typedef для std::pair<const int, int>, а std::make_pair(), очевидно, производит std::pair<> через магию вычитания шаблона. Конечный результат, однако, должен быть одинаковым для версий 2, 3 и 4.

Какой из них я бы использовал? Я лично предпочитаю версию 1; это кратким и "естественным". Конечно, если его поведение переписывания нежелательно, я бы предпочел версию 4, так как он требует меньше ввода текста, чем версии 2 и 3. Я не знаю, есть ли один де-факто способ вставки пар ключ/значение в std::map.

Другой способ вставки значений в карту через один из его конструкторов:

std::map<int, int> quadratic_func;

quadratic_func[0] = 0;
quadratic_func[1] = 1;
quadratic_func[2] = 4;
quadratic_func[3] = 9;

std::map<int, int> my_func(quadratic_func.begin(), quadratic_func.end());

Ответ 4

Я провел некоторое сравнение времени между вышеупомянутыми версиями:

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

Оказывается, что временные различия между версиями вставки крошечные.

#include <map>
#include <vector>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;
class Widget {
public:
    Widget() {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = 1.0;
        }
    }
    Widget(double el)   {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = el;
        }
    }
private:
    std::vector<double> m_vec;
};


int main(int argc, char* argv[]) {



    std::map<int,Widget> map_W;
    ptime t1 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));
    }
    ptime t2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff = t2 - t1;
    std::cout << diff.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_2;
    ptime t1_2 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_2.insert(std::make_pair(it,Widget(2.0)));
    }
    ptime t2_2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_2 = t2_2 - t1_2;
    std::cout << diff_2.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_3;
    ptime t1_3 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_3[it] = Widget(2.0);
    }
    ptime t2_3 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_3 = t2_3 - t1_3;
    std::cout << diff_3.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_0;
    ptime t1_0 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));
    }
    ptime t2_0 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_0 = t2_0 - t1_0;
    std::cout << diff_0.total_milliseconds() << std::endl;

    system("pause");
}

Это дает соответственно версии (я запустил файл 3 раза, следовательно, 3 последовательных разницы во времени для каждого):

map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));

2198 мс, 2078 мс, 2072 мс

map_W_2.insert(std::make_pair(it,Widget(2.0)));

2290 мс, 2037 мс, 2046 мс

 map_W_3[it] = Widget(2.0);

2592 мс, 2278 мс, 2296 мс

 map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));

2234 мс, 2031 мс, 2027 мс

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

Версия map_W_3[it] = Widget(2.0); занимает примерно 10-15% больше времени для этого примера из-за инициализации со стандартным конструктором для Widget.

Ответ 5

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

Цитированный фрагмент из Effective STL: 50 конкретных способов улучшить использование стандартной библиотеки шаблонов Скотта Мейерса, пункт 24, может помочь.

template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapType::iterator
insertKeyAndValue(MapType& m, const KeyArgType&k, const ValueArgType& v)
{
    typename MapType::iterator lb = m.lower_bound(k);

    if (lb != m.end() && !(m.key_comp()(k, lb->first))) {
        lb->second = v;
        return lb;
    } else {
        typedef typename MapType::value_type MVT;
        return m.insert(lb, MVT(k, v));
    }
}

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

Ответ 6

Если вы хотите перезаписать элемент клавишей 0

function[0] = 42;

В противном случае:

function.insert(std::make_pair(0, 42));

Ответ 7

Если вы хотите вставить элемент в std:: map - используйте функцию insert(), и если вы хотите найти элемент (по ключу) и назначить ему что-то - используйте operator [].

Для упрощения вставки используйте boost:: assign library, например:

using namespace boost::assign;

// For inserting one element:

insert( function )( 0, 41 );

// For inserting several elements:

insert( function )( 0, 41 )( 0, 42 )( 0, 43 );

Ответ 8

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

std::map<int, std::string> rancking;

rancking[0] = 42;  // << some compilers [gcc] show no error

rancking.insert(std::pair<int, std::string>(0, 42));// always a compile error

тот факт, что компилятор не обнаруживает ошибки при "rancking [1] = 42;" может иметь разрушительный эффект!

Ответ 9

Поскольку C++ 17 std::map предлагает два новых метода вставки: insert_or_assign() и try_emplace(), как также упоминалось в комментарий от sp2danny.

insert_or_assign()

По сути, insert_or_assign() является "улучшенной" версией operator[]. В отличие от operator[], insert_or_assign() не требует, чтобы тип значения карты был конструируемым по умолчанию. Например, следующий код не компилируется, потому что MyClass не имеет конструктора по умолчанию:

class MyClass {
public:
    MyClass(int i) : m_i(i) {};
    int m_i;
};

int main() {
    std::map<int, MyClass> myMap;

    // VS2017: "C2512: 'MyClass::MyClass' : no appropriate default constructor available"
    // Coliru: "error: no matching function for call to 'MyClass::MyClass()"
    myMap[0] = MyClass(1);

    return 0;
}

Однако, если вы замените myMap[0] = MyClass(1); следующей строкой, код скомпилируется, и вставка произойдет, как и предполагалось:

myMap.insert_or_assign(0, MyClass(1));

Более того, подобно insert(), insert_or_assign() возвращает pair<iterator, bool>. Логическое значение - true, если вставка произошла, и false, если назначение было выполнено. Итератор указывает на элемент, который был вставлен или обновлен.

try_emplace()

Как и выше, try_emplace() является "улучшением" emplace(). В отличие от emplace(), try_emplace() не изменяет свои аргументы, если вставка не удалась из-за ключа, уже существующего на карте. Например, следующий код пытается добавить элемент с ключом, который уже хранится на карте (см. *):

int main() {
    std::map<int, std::unique_ptr<MyClass>> myMap2;
    myMap2.emplace(0, std::make_unique<MyClass>(1));

    auto pMyObj = std::make_unique<MyClass>(2);    
    auto [it, b] = myMap2.emplace(0, std::move(pMyObj));  // *

    if (!b)
        std::cout << "pMyObj was not inserted" << std::endl;

    if (pMyObj == nullptr)
        std::cout << "pMyObj was modified anyway" << std::endl;
    else
        std::cout << "pMyObj.m_i = " << pMyObj->m_i <<  std::endl;

    return 0;
}

Вывод (по крайней мере для VS2017 и Coliru):

pMyObj не был вставлен
В любом случае pMyObj был изменен

Как видите, pMyObj больше не указывает на исходный объект. Однако если вы замените auto [it, b] = myMap2.emplace(0, std::move(pMyObj)); на следующий код, то результат будет выглядеть иначе, потому что pMyObj остается неизменным:

auto [it, b] = myMap2.try_emplace(0, std::move(pMyObj));

Выход:

pMyObj was not inserted
pMyObj pMyObj.m_i = 2

Код на Колиру

Пожалуйста, обратите внимание: я старался сделать свои объяснения максимально короткими и простыми, чтобы вписать их в этот ответ. Для более точного и всестороннего описания я рекомендую прочитать эту статью на свободном языке C++.