Який переважний / ідіоматичний спосіб вставити у карту?


111

Я визначив чотири різні способи вставки елементів у 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));

Який із них є кращим / ідіоматичним? (А чи є інший спосіб, про який я не думав?)


26
Вашу карту слід називати "відповідями", а не "функцією"
Вінсент Роберт

2
@ Вінсент: Гм? Функція в основному є картою між двома множинами.
fredoverflow

7
@FredOverflow: здається, коментар Вінсента - це якийсь жарт про певну книгу ...
Віктор Сорокін

1
Здається, це суперечить оригіналу - 42 одночасно не можуть бути відповіддю на (а) життя, Всесвіт і все, і (б) нічого. Але як тоді ви виражаєте життя, Всесвіт і все як інту?
Стюарт Голодець

19
@sgolodetz Ви можете виразити все з достатньо великим int.
Яків Галка

Відповіді:


90

Перш за все, 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- це, мабуть, шлях.


чи справді вимагає перетворення від Key до const Key у make_pair () інший виклик функції? Здається, що достатньо неявного амплуа, який компілятор повинен із задоволенням зробити це.
галактика

99

Що стосується C ++ 11, у вас є два основні додаткові варіанти. По-перше, ви можете використовувати insert()з синтаксисом ініціалізації списку:

function.insert({0, 42});

Це функціонально рівнозначно

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

але набагато більш стислим і читабельним. Як відзначали інші відповіді, це має ряд переваг перед іншими формами:

  • The operator[]Підхід вимагає відображений типу бути призначуваними, це не завжди так.
  • The operator[]Підхід може замінити існуючі елементи, і не дає ніякого способу дізнатися , є чи це сталося.
  • Інші форми, insertякі ви перераховуєте, передбачають неявне перетворення типу, яке може сповільнити ваш код.

Основним недоліком є ​​те, що ця форма вимагала копіювати ключ і значення, тому вона не працюватиме, наприклад, з картою зі unique_ptrзначеннями. Це було виправлено у стандарті, але виправлення, можливо, ще не дійшло до вашої стандартної бібліотеки.

По-друге, ви можете використовувати emplace()метод:

function.emplace(0, 42);

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


8
є також новий insert_or_assign та try_emplace
sp2danny

11

Перша версія:

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());

5

Якщо ви хочете перезаписати елемент клавішею 0

function[0] = 42;

Інакше:

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

5

Оскільки 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()повертає a 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 не було вставлено
pMyObj pMyObj.m_i = 2

Код про Коліру

Зверніть увагу: я намагався зробити свої пояснення максимально короткими та простими, щоб вписати їх у цю відповідь. Для більш точного та вичерпного опису рекомендую прочитати цю статтю на Fluent C ++ .


3

Я вже проводив порівняння між вищезгаданими версіями:

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% більше часу для цього прикладу з - за ініціалізації з допомогою конструктора за замовчуванням для віджета.


2

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

Процитований фрагмент " Ефективна програма 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));
    }
}

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


1

Якщо ви хочете вставити елемент у std :: map - використовуйте функцію insert (), а якщо ви хочете знайти елемент (за клавішею) та призначити йому деякий - скористайтеся оператором [].

Для спрощення вставки використовуйте boost :: призначити бібліотеку, як це:

using namespace boost::assign;

// For inserting one element:

insert( function )( 0, 41 );

// For inserting several elements:

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

1

Я просто трохи змінив проблему (карта рядків), щоб показати інший інтерес до вставки:

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

той факт, що компілятор не показує помилки при "ранжируванні [1] = 42;" може мати руйнівний вплив!


Компілятори не показують помилку для першого, оскільки std::string::operator=(char)існує, але вони показують помилку для другого, оскільки конструктор std::string::string(char)не існує. Він не повинен створювати помилки, оскільки C ++ завжди вільно інтерпретує будь-який літерал цілого стилю як char, тому це не помилка компілятора, а натомість помилка програміста. В основному, я просто кажу, що чи ні це вводить помилку у ваш код - це те, на що потрібно стежити за собою. До речі, ви можете надрукувати rancking[0]та виведе компілятор за допомогою ASCII *, який є (char)(42).
Кіт М

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.