вставити vs emplace проти оператора [] у карту c ++


191

Я вперше використовую карти і зрозумів, що існує багато способів вставити елемент. Ви можете використовувати emplace(), operator[]або insert(), плюс такі варіанти, як використання value_typeабо make_pair. Незважаючи на те, що про них є багато інформації, а також питання щодо конкретних випадків, я все ще не можу зрозуміти велику картину. Отже, моє два питання:

  1. Яка перевага кожного з них перед іншими?

  2. Чи була потреба в додаванні заміни до стандарту? Чи є щось, що раніше було неможливо без цього?


1
Семантика зміщення дозволяє явні перетворення та пряму ініціалізацію.
Керрек СБ

3
Тепер operator[]базується на try_emplace. Це, можливо, варто також згадати insert_or_assign.
FrankHB

@FrankHB, якщо ви (чи хтось інший) додасте сучасну відповідь, я можу змінити прийняту.
Німецький Капуано

Відповіді:


228

У конкретному випадку на карті старих варіантів було лише два: operator[]і insert(різні смаки insert). Тож я почну пояснювати це.

The operator[]Є знахідкою або, додати оператор. Він спробує знайти елемент із заданим ключем всередині карти, і якщо він існує, він поверне посилання на збережене значення. Якщо цього не зробити, він створить новий елемент, вставлений на місце з ініціалізацією за замовчуванням, і поверне посилання на нього.

insertФункція (в єдиному елементі аромату) займає value_type(std::pair<const Key,Value> ), він використовує ключ ( firstелемент) і намагається вставити її. Оскільки std::mapне дає дублікатів, якщо є існуючий елемент, він нічого не вставить.

Перша відмінність між ними полягає в тому, що operator[]потрібно мати можливість побудувати ініціалізоване значення за замовчуванням , і це, таким чином, непридатне для типів значень, які неможливо ініціалізувати за замовчуванням. Друга різниця між ними - це те, що відбувається, коли вже є елемент із заданим ключем. insertФункція не змінюватиме стан карти, але замість того, щоб повернути итератор до елементу (і falseпро те , що вона не була встановлена).

// assume m is std::map<int,int> already has an element with key 5 and value 0
m[5] = 10;                      // postcondition: m[5] == 10
m.insert(std::make_pair(5,15)); // m[5] is still 10

У випадку insertаргументу - це об'єкт value_type, який можна створити різними способами. Ви можете безпосередньо сконструювати його з відповідним типом або передати будь-який об’єкт, з якого value_typeможна побудувати, який є де std::make_pairвступає в гру, оскільки це дозволяє просте створення std::pairоб'єктів, хоча це, мабуть, не те, що ви хочете ...

Чистий ефект таких дзвінків схожий :

K t; V u;
std::map<K,V> m;           // std::map<K,V>::value_type is std::pair<const K,V>

m.insert( std::pair<const K,V>(t,u) );      // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) );            // 3

Але насправді не однакові ... [1] та [2] насправді є рівнозначними. В обох випадках код створює тимчасовий об'єкт одного типу ( std::pair<const K,V>) і передає його insertфункції. insertФункція створює відповідний вузол в бінарному дереві пошуку , а потім скопіювати value_typeчастину з аргументу в вузол. Перевага використання value_typeв тому, що, мабуть, value_typeзавжди збігається value_type , ти не можеш помилково ввести тип std::pairаргументів!

Різниця полягає в [3]. Функція std::make_pair- це шаблонна функція, яка створить a std::pair. Підпис:

template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );

Я навмисно не надав аргументів шаблону std::make_pair, оскільки це звичайне використання. І сенс полягає в тому, що аргументи шаблону виводяться з виклику, в цьому випадку - T==K,U==Vтак, тож виклик до std::make_pairповерне а std::pair<K,V>(відзначте відсутні const). Підпис вимагає, value_typeщоб воно було близьким, але не було таким, як повернене значення від виклику до std::make_pair. Оскільки він досить близький, він створить тимчасовий правильний тип і копіює його ініціалізувати. Це в свою чергу буде скопійовано у вузол, створивши загалом дві копії.

Це можна виправити, надавши аргументи шаблону:

m.insert( std::make_pair<const K,V>(t,u) );  // 4

Але це все ще схильна до помилок так само, як явно вводити тип у випадку [1].

До цього моменту у нас є різні способи виклику, insertякі вимагають створення value_typeзовнішньої сторони та копії цього об'єкта в контейнер. Крім того, ви можете використовувати, operator[]якщо тип за замовчуванням може бути сконструйований і призначений (навмисно фокусується лише в m[k]=v), і для цього потрібна ініціалізація одного об'єкта та копії за замовчуванням значення в цей об’єкт.

У C ++ 11, з різноманітними шаблонами та ідеальним переадресацією, є новий спосіб додавання елементів у контейнер за допомогою введення (створення на місці). Ці emplaceфункції в різних контейнерах роблять в основному те ж саме: замість того , щоб отримати джерело , з якого скопіювати в контейнер, функція приймає параметри , які будуть передані в конструктор об'єкта , що зберігається в контейнері.

m.emplace(t,u);               // 5

У роботі [5], std::pair<const K, V>не створюється і передається до emplace, а, скоріше, посилання на об'єкт tі uоб'єкт передаються тому, emplaceщо пересилає їх конструктору value_typeсубекти в структурі даних. У цьому випадку немає Копії std::pair<const K,V>не виконуються взагалі, що є перевагою emplaceнад C ++ 03 альтернатив. Як і у випадку з insertцим, це не перекриє значення на карті.


Цікаве питання, над яким я не замислювався, - як emplaceнасправді реалізувати карту, і це не є простою проблемою в загальному випадку.


5
Це натякає у відповіді, але map [] = val замінить попереднє значення, якщо воно існує.
dk123

Більш цікаве питання в моєму розумінні - це те, що воно служить малій меті. Тому що ви зберігаєте парну копію, що добре, тому що жодна копія пари не mapped_typeозначає копії. Що ми хочемо, це замінити конструкцію mapped_typeпари та замінити парну конструкцію на карті. Тому std::pair::emplaceфункція та її підтримка переадресації в map::emplaceобох відсутні. У його теперішньому вигляді вам все одно потрібно надати побудований mapped_type парному конструктору, який скопіює його один раз. це краще, ніж удвічі, але все-таки нічого доброго.
v.oddou

насправді я змінюю цей коментар, у C ++ 11 є конструктор парних шаблонів, який виконує точно таку ж ціль, що і замість у випадку побудови 1 аргументу. і деякі дивні кусково-конструктивні, як вони це називають, використовуючи кортежі для передачі аргументів, тож ми все ще можемо мати ідеальне пересилання.
v.oddou

Схоже, є помилка продуктивності вставки в unorported_map та map: посилання
Deqing

1
Можливо, приємно оновити це інформацією про insert_or_assignта try_emplace(як від C ++ 17), які допоможуть заповнити деякі прогалини у функціональності від існуючих методів.
ShadowRanger

15

Emplace: використовує посилання на рецензію, щоб використовувати фактично створені вами об'єкти. Це означає, що жоден конструктор копіювання чи переміщення не викликається, що добре для ВЕЛИКІ об’єктів! Час (log (N)).

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

Оператор []: Перевіряє, чи існує об'єкт, і якщо він є, змінює посилання на цей об'єкт, інакше використовує наданий ключ і значення для виклику make_pair на двох об'єктах, а потім виконує ту ж роботу, що і функція вставки. Це час O (log (N)).

make_pair: робить трохи більше, ніж робити пару.

Не було "потреби" в додаванні місця до стандарту. В c ++ 11 я вважаю, що тип && посилання було додано. Це усунуло необхідність руху семантики і дозволило оптимізувати певний тип управління пам’яттю. Зокрема, посилання на оцінку. Оператор перевантаженої вставки (value_type &&) не використовує переваги семантики in_place, а тому набагато менш ефективний. Незважаючи на те, що вона забезпечує можливість поводження з посиланнями на реальну оцінку, вона ігнорує їх ключове призначення, яке полягає в побудові об'єктів.


4
" Не було" потреби "в додаванні місця до стандарту". Це явно неправдиво. emplace()це просто єдиний спосіб вставити елемент, який неможливо скопіювати або перемістити. (І так, мабуть, найбільш ефективно вставити той, чиї конструктори копіювання та переміщення коштують набагато дорожче, ніж конструкція, якщо така річ існує). Здається, ви зрозуміли, що у вас ідея неправильна: справа не в тому, щоб " скористатися] перевагою посилання на оцінку використовувати фактично створені вами об’єкти "; ні один об'єкт не створено, і ви перенаправляти mapаргументи він повинен створити його всередині себе. Ви не робите об'єкт.
підкреслюйте_2

10

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

Ось приклад для демонстрації:

#include <vector>

struct foo
{
    explicit foo(int);
};

int main()
{
    std::vector<foo> v;

    v.emplace(v.end(), 10);      // Works
    //v.insert(v.end(), 10);     // Error, not explicit
    v.insert(v.end(), foo(10));  // Also works
}

Це, звичайно, дуже конкретна деталь, але коли ви маєте справу з ланцюжками конверсій, визначених користувачем, варто пам’ятати про це.


Уявіть, що foo потрібні два ints у своєму ctor, а не один. Чи могли б ви використовувати цей дзвінок? v.emplace(v.end(), 10, 10); ... чи вам зараз потрібно буде використовувати v.emplace(v.end(), foo(10, 10) ); :?
Кайтан

Зараз у мене немає доступу до компілятора, але я вважаю, що це означає, що обидві версії будуть працювати. Практично всі приклади, які ви бачите, emplaceвикористовують клас, який приймає один параметр. IMO, це фактично зробить характер варіатичного синтаксису emplace набагато зрозумілішим, якби в прикладах використовувались декілька параметрів.
Кайтан

9

Наступний код може допомогти вам зрозуміти, наскільки «ідея великої картини» insert()відрізняється від emplace():

#include <iostream>
#include <unordered_map>
#include <utility>

//Foo simply outputs what constructor is called with what value.
struct Foo {
  static int foo_counter; //Track how many Foo objects have been created.
  int val; //This Foo object was the val-th Foo object to be created.

  Foo() { val = foo_counter++;
    std::cout << "Foo() with val:                " << val << '\n';
  }
  Foo(int value) : val(value) { foo_counter++;
    std::cout << "Foo(int) with val:             " << val << '\n';
  }
  Foo(Foo& f2) { val = foo_counter++;
    std::cout << "Foo(Foo &) with val:           " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(const Foo& f2) { val = foo_counter++;
    std::cout << "Foo(const Foo &) with val:     " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(Foo&& f2) { val = foo_counter++;
    std::cout << "Foo(Foo&&) moving:             " << f2.val
              << " \tand changing it to:\t" << val << '\n';
  }
  ~Foo() { std::cout << "~Foo() destroying:             " << val << '\n'; }

  Foo& operator=(const Foo& rhs) {
    std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
              << " \tcalled with lhs.val = \t" << val
              << " \tChanging lhs.val to: \t" << rhs.val << '\n';
    val = rhs.val;
    return *this;
  }

  bool operator==(const Foo &rhs) const { return val == rhs.val; }
  bool operator<(const Foo &rhs)  const { return val < rhs.val;  }
};

int Foo::foo_counter = 0;

//Create a hash function for Foo in order to use Foo with unordered_map
namespace std {
   template<> struct hash<Foo> {
       std::size_t operator()(const Foo &f) const {
           return std::hash<int>{}(f.val);
       }
   };
}

int main()
{
    std::unordered_map<Foo, int> umap;  
    Foo foo0, foo1, foo2, foo3;
    int d;

    //Print the statement to be executed and then execute it.

    std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
    umap.insert(std::pair<Foo, int>(foo0, d));
    //Side note: equiv. to: umap.insert(std::make_pair(foo0, d));

    std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
    umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
    //Side note: equiv. to: umap.insert(std::make_pair(foo1, d));

    std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
    std::pair<Foo, int> pair(foo2, d);

    std::cout << "\numap.insert(pair)\n";
    umap.insert(pair);

    std::cout << "\numap.emplace(foo3, d)\n";
    umap.emplace(foo3, d);

    std::cout << "\numap.emplace(11, d)\n";
    umap.emplace(11, d);

    std::cout << "\numap.insert({12, d})\n";
    umap.insert({12, d});

    std::cout.flush();
}

Вихід, який я отримав:

Foo() with val:                0
Foo() with val:                1
Foo() with val:                2
Foo() with val:                3

umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val:           4    created from:       0
Foo(Foo&&) moving:             4    and changing it to: 5
~Foo() destroying:             4

umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val:           6    created from:       1
Foo(Foo&&) moving:             6    and changing it to: 7
~Foo() destroying:             6

std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val:           8    created from:       2

umap.insert(pair)
Foo(const Foo &) with val:     9    created from:       8

umap.emplace(foo3, d)
Foo(Foo &) with val:           10   created from:       3

umap.emplace(11, d)
Foo(int) with val:             11

umap.insert({12, d})
Foo(int) with val:             12
Foo(const Foo &) with val:     13   created from:       12
~Foo() destroying:             12

~Foo() destroying:             8
~Foo() destroying:             3
~Foo() destroying:             2
~Foo() destroying:             1
~Foo() destroying:             0
~Foo() destroying:             13
~Foo() destroying:             11
~Foo() destroying:             5
~Foo() destroying:             10
~Foo() destroying:             7
~Foo() destroying:             9

Зауважте, що:

  1. An unordered_mapзавжди внутрішньо зберігає Fooоб'єкти (а не, скажімо, Foo *з) як ключі, які все знищено , коли unordered_mapбуде знищений. Тут unordered_mapвнутрішніми ключами були foos 13, 11, 5, 10, 7 і 9.

    • Таким чином, технічно наші unordered_mapфактично зберігають std::pair<const Foo, int>предмети, які в свою чергу зберігають Fooоб'єкти. Але, щоб зрозуміти, наскільки «ідея великої картини» emplace()відрізняється від insert()(див. Виділене поле нижче), нормально тимчасово уявити цей std::pairоб’єкт як повністю пасивний. Після того, як ви зрозумієте цю «ідею великої картини», важливо потім створити резервну копію та зрозуміти, як використання цього std::pairоб’єкта- посередника шляхом unordered_mapвведення тонких, але важливих технічних можливостей.
  2. Установка кожного з foo0, foo1і foo2потрібно 2 виклику до одного з Foo«копіювання / переміщення конструкторами сек і 2 виклики на Foo" s деструктор (як я тепер описати):

    а. Вставляючи кожен із них foo0і foo1створюючи тимчасовий об'єкт ( foo4і foo6відповідно), деструктор був негайно викликаний після завершення вставки. Крім того, внутрішні Foos (не Foo5, 7 і 7) також мали свої деструктори, що викликаються, коли unordered_map було знищено.

    б. Щоб вставити foo2, ми замість цього спочатку явно створили не тимчасовий парний об'єкт (так званий pair), який викликав Fooконструктор копіювання на foo2(створюючи foo8як внутрішній член pair). Потім ми insert()редагували цю пару, в результаті чого unordered_mapконструктор копіювання знову (увімкнено foo8) викликав свою внутрішню копію ( foo9). Як і у foos 0 та 1, кінцевим результатом було два виклики деструктора для цієї вставки, з тією лише різницею, що foo8деструктор викликався лише тоді, коли ми дійшли до кінця, main()а не викликався відразу після insert()закінчення.

  3. В foo3результаті використання лише 1 виклик конструктора копіювання / переміщення (створення foo10внутрішньо у програмі unordered_map) та лише 1 виклик Fooдеструктора s. (Я повернусь до цього пізніше).

  4. Тому що foo11ми безпосередньо передали ціле число 11 emplace(11, d)так, щоб unordered_mapвикликати Foo(int)конструктор, поки виконання знаходиться в межах його emplace()методу. На відміну від (2) та (3), для цього нам навіть не потрібен був якийсь попередній fooоб'єкт. Важливо відзначити, що Fooвідбувся (який створив foo11) лише 1 виклик конструктору .

  5. Потім ми безпосередньо передали ціле число 12 до insert({12, d}). На відміну від emplace(11, d)(який викликав лише один виклик Fooконструктору), цей виклик insert({12, d})призвів до двох викликів Fooконструктору s (створення foo12та foo13).

Це показує , що основна «велика картина» різниця між insert()і emplace()є:

Тоді як використання insert() майже завжди вимагає побудови або існування якогось Fooоб'єкта в main()області дії (з наступним копіюванням або переміщенням), якщо використання emplace()будь-якого виклику Fooконструктору виконується повністю всередині unordered_map(тобто всередині сфери визначення emplace()методу). Аргументи (и) для ключа, який ви emplace()передаєте, безпосередньо пересилаються на Fooвиклик конструктора в межах unordered_map::emplace()визначення (необов'язкові додаткові деталі: де цей щойно побудований об'єкт негайно включається в одну з unordered_mapзмінних-учасників, так що не викликається деструктор, коли виконання залишає emplace()і конструктори переміщення чи копіювання не викликаються).

Примітка: Причина для " майже " в " майже завжди " вище пояснюється в I) нижче.

  1. продовження: Причина , чому покликання umap.emplace(foo3, d)називається Foo«s неконстантная конструктор копіювання є наступне: Так як ми використовуємо emplace(), компілятор знає , що foo3(неконстантний Fooоб'єкт) призначається , щоб бути аргументом в якій - то Fooконструктор. У цьому випадку найбільш підходящим Fooконструктором є неконструкторний конструктор копій Foo(Foo& f2). Ось чому umap.emplace(foo3, d)викликали конструктор копій, поки umap.emplace(11, d)цього не зробили.

Епілог:

I. Зауважте, що одне перевантаження insert()фактично еквівалентно emplace() . Як описано на цій сторінці cppreference.com , перевантаження template<class P> std::pair<iterator, bool> insert(P&& value)(яке перевантаження (2) insert()на цій сторінці cppreference.com) еквівалентно emplace(std::forward<P>(value)).

II. Куди піти звідси?

а. Пограйте з вищезазначеним вихідним кодом та вивчіть документацію insert()(наприклад, тут ) та emplace()(наприклад, тут ), які можна знайти в Інтернеті. Якщо ви використовуєте IDE, наприклад eclipse або NetBeans, ви можете легко отримати свій IDE, щоб повідомити, яке перевантаження insert()або emplace()викликається (при затемненні, просто тримайте курсор миші стійким над викликом функції на секунду). Ось ще декілька кодів для випробування:

std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
//but umap.emplace({{Foo::foo_counter, d}}); results in a compile error!

std::cout << "\numap.insert(std::pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<const Foo, int>({Foo::foo_counter, d}));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&). 
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
//Not only that, but even more interesting is how the call below uses all 
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy 
// constructors, despite the below call's only difference from the call above 
// being the additional { }.
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});


//Pay close attention to the subtle difference in the effects of the next 
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}}) where " 
  << "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}});

std::cout << "\numap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}}) where "
  << "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}});


//umap.insert(std::initializer_list<std::pair<Foo, int>>({{Foo::foo_counter, d}}));
//The call below works fine, but the commented out line above gives a 
// compiler error. It's instructive to find out why. The two calls
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>({{Foo::foo_counter, d}}));

Незабаром ви побачите, що перевантаження std::pairконструктора (див. Посилання ) в кінцевому підсумку використовується, unordered_mapможе мати важливий вплив на кількість об'єктів, які копіюються, переміщуються, створюються та / або знищуються, а також коли це все відбувається.

б. Подивіться, що станеться, якщо ви використовуєте інший клас контейнерів (наприклад, std::setабо std::unordered_multiset) замість std::unordered_map.

c. Тепер використовуйте Gooоб'єкт (просто перейменовану копію Foo) замість intтипу діапазону в unordered_map(тобто використання unordered_map<Foo, Goo>замість unordered_map<Foo, int>) і подивіться, скільки і які Gooконструктори викликаються. (Спойлер: ефект є, але це не дуже драматично.)


0

З точки зору функціональності та виходу, вони обидва однакові.

Для обох великих об'ємів пам'ять об'єкта оптимізована пам'ять, яка не використовує конструктори копій

Для простого детального пояснення https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44

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