insert vs emplace vs operator [] w C++ Mapa
Używam map po raz pierwszy i zdałem sobie sprawę, że jest wiele sposobów na wstawienie elementu. Możesz użyć emplace()
, operator[]
lub insert()
, plus warianty takie jak value_type
lub make_pair
. Chociaż jest wiele informacji o nich wszystkich i pytań dotyczących poszczególnych przypadków, nadal nie mogę zrozumieć szerszego obrazu.
Więc moje dwa pytania to:
Jaka jest przewaga każdego z nich nad innymi?
Czy była potrzeba dodania emplace do standardu? Czy jest coś, co nie było możliwe wcześniej bez tego?
4 answers
W konkretnym przypadku mapy stare opcje były tylko dwa: operator[]
i insert
(różne smaki insert
). Więc zacznę je wyjaśniać.
operator[]
jest operatorem find-or-add . Spróbuje znaleźć element z podanym kluczem wewnątrz mapy, a jeśli taki istnieje, zwróci odniesienie do przechowywanej wartości. Jeśli nie, utworzy nowy element wstawiony w miejsce z domyślną inicjalizacją i zwróci do niego odniesienie.
Funkcja insert
(w smaku jednego elementu) bierze value_type
(std::pair<const Key,Value>
), używa klucza (first
member) i próbuje go wstawić. Ponieważ std::map
nie pozwala na duplikaty jeśli istnieje istniejący element, to niczego nie wstawi.
Pierwsza różnica między tymi dwoma jest taka, że operator[]
musi być w stanie skonstruować domyślną zainicjalizowaną wartość , a zatem jest bezużyteczna dla typów wartości, które nie mogą być zainicjalizowane domyślnie. Drugą różnicą między nimi jest to, co się dzieje, gdy jest już element z danym kluczem. Funkcja insert
nie zmieni stanu mapy, ale zwróci iterator do elementu (i false
wskazując, że nie został wstawiony).
// 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
W przypadku insert
argument jest obiektem value_type
, który można utworzyć na różne sposoby. Można go bezpośrednio skonstruować za pomocą odpowiedniego typu lub przekazać dowolny obiekt, z którego można zbudować value_type
, czyli gdzie std::make_pair
wchodzi w grę, ponieważ pozwala na proste tworzenie obiektów std::pair
, chociaż prawdopodobnie nie jest to to, czego chcesz...
Efekt netto następujących wywołań jest podobny :
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
Ale nie są tak naprawdę takie same... [1] i [2] są w rzeczywistości równoważne. W obu przypadkach kod tworzy obiekt tymczasowy tego samego typu (std::pair<const K,V>
) i przekazuje go do funkcji insert
. Funkcja insert
utworzy odpowiedni węzeł w drzewie wyszukiwania binarnego, a następnie skopiuje część value_type
z argumentu do węzła. Na zaletą używania value_type
jest to, że value_type
zawsze pasuje value_type
, nie można pomylić typu argumentów std::pair
!
Różnica jest w [3]. Funkcja {[20] } jest funkcją szablonową, która utworzy std::pair
. Podpis:
template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );
Celowo nie podałem argumentów szablonu std::make_pair
, ponieważ jest to powszechne użycie. A implikacją jest to, że argumenty szablonu są wyprowadzane z wywołania, w tym przypadku to T==K,U==V
, więc wywołanie to std::make_pair
zwróci std::pair<K,V>
(zwróć uwagę na brakujące const
). Podpis wymaga value_type
, czyli Zamknij , ale nie to samo, co wartość zwracana z wywołania do std::make_pair
. Ponieważ jest wystarczająco blisko, utworzy tymczasowy odpowiedni typ i skopiuje go. To z kolei zostanie skopiowane do węzła, tworząc w sumie dwie kopie.
Można to naprawić podając argumenty szablonu:
m.insert( std::make_pair<const K,V>(t,u) ); // 4
Ale to wciąż podatne na błędy w ten sam sposób, że wyraźnie wpisując typ w przypadku [1].
Do tego momentu mamy różne sposoby wywoływania insert
, które wymagają utworzenia value_type
zewnętrznie i skopiowania tego obiektu do kontenera. Alternatywnie można użyć operator[]
, Jeśli typ jest domyślną konstruktywną i przypisywalną (celowe skupienie tylko w m[k]=v
), i wymaga domyślnej inicjalizacji jednego obiektu i skopiowania wartości do tego obiektu.
W C++11, ze zmiennymi szablony i doskonałe przekierowanie istnieje nowy sposób dodawania elementów do kontenera za pomocą emplacing (tworzenie w miejscu). Funkcje emplace
w różnych kontenerach robią zasadniczo to samo: zamiast pobierać źródło, z którego do kopiuje do kontenera, funkcja pobiera parametry, które zostaną przekazane do konstruktora obiektu przechowywanego w kontenerze.
m.emplace(t,u); // 5
W [5], std::pair<const K, V>
nie jest tworzony i przekazywany do emplace
, ale raczej odniesienia do t
i u
są przekazywane do emplace
, który przekazuje je do konstruktora podobiektu value_type
wewnątrz struktury danych. W tym przypadku nie ma kopii std::pair<const K,V>
, co jest zaletą emplace
nad alternatywami C++03. Podobnie jak w przypadku insert
nie nadpisuje wartości na mapie.
Ciekawym pytaniem, o którym nie myślałem, jest to, w jaki sposób można zaimplementować mapę, a nie jest to prosty problem w ogólnym przypadku.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-18 16:33:20
Emplace: wykorzystuje odniesienie rvalue do użycia rzeczywistych obiektów, które już utworzyłeś. Oznacza to, że nie jest wywoływany konstruktor kopiowania lub przenoszenia, dobry dla dużych obiektów! O(log (N)) czas.
Insert: posiada przeciążenia dla standardowych referencji lvalue i rvalue, a także Iteratory do list elementów do wstawienia oraz "podpowiedzi"co do pozycji elementu. Użycie iteratora "podpowiedzi" może sprowadzić czas wstawiania do czasu kontantowego, w przeciwnym razie jest O(log (N)) czas.
Operator []: sprawdza, czy obiekt istnieje, a jeśli tak, modyfikuje odniesienie do tego obiektu, w przeciwnym razie używa podanego klucza i wartości do wywołania make_pair na obu obiektach, a następnie wykonuje tę samą pracę, co funkcja insert. To jest czas o(log(N)).
Make_pair: robi niewiele więcej niż tworzy parę.
Nie było "potrzeby" dodawania emplace do standardu. W c++11 uważam, że dodano & & Typ odniesienia. To usunęło konieczność semantyki ruchu i umożliwiła optymalizację pewnego określonego rodzaju zarządzania pamięcią. W szczególności odniesienie rvalue. Przeciążony operator insert (value_type &&) nie wykorzystuje semantyki in_place i dlatego jest znacznie mniej wydajny. Chociaż zapewnia możliwość radzenia sobie z referencjami rvalue, ignoruje ich kluczowy cel, jakim jest budowa obiektów.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-18 15:45:05
Oprócz możliwości optymalizacji i prostszej składni, ważną różnicą między wstawianiem i umieszczaniem jest to, że ta ostatnia pozwala na konwersje jawne . (Dotyczy to całej biblioteki standardowej, nie tylko map.)
Oto przykład do zademonstrowania:
#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
}
Jest to wprawdzie bardzo konkretny szczegół, ale kiedy masz do czynienia z łańcuchami konwersji zdefiniowanych przez użytkownika, warto o tym pamiętać.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-29 08:46:10
Poniższy kod może pomóc ci zrozumieć "big picture idea" czym insert()
różni się od emplace()
:
#include <iostream>
#include <unordered_map>
#include <utility>
struct Foo {
static int foo_counter;
int val;
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;
std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
umap.insert(std::pair<Foo, int>(foo0, d));
//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)));
//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();
}
Wyjście, które dostałem było:
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
Zauważ, że:
-
An
unordered_map
zawsze wewnętrznie przechowujeFoo
obiekty (a nie, powiedzmy,Foo *
s) jako klucze, które są niszczone, gdyunordered_map
jest niszczone. Tutaj, klucze wewnętrzneunordered_map
były foos 13, 11, 5, 10, 7, i 9.- więc technicznie, nasz
unordered_map
faktycznie przechowujestd::pair<const Foo, int>
obiekty, które z kolei przechowujeFoo
obiekty. Aby jednak zrozumieć, czymemplace()
różni się odinsert()
(patrz podświetlona ramka poniżej), w porządku jest tymczasowo wyobrazić sobie tenstd::pair
obiekt jako całkowicie pasywny. Kiedy już zrozumiesz tę "ideę big picture", ważne jest, aby wykonać kopię zapasową i zrozumieć, w jaki sposób użycie tego pośredniego obiektustd::pair
przezunordered_map
wprowadza subtelne, ale ważne szczegóły techniczne.
- więc technicznie, nasz
-
Wstawianie każdego z
foo0
,foo1
, orazfoo2
wymagane 2 wywołania do jednego z konstruktorówFoo
i 2 wywołania do destruktoraFoo
(Jak teraz opisuję):A. wstawienie każdego z
foo0
ifoo1
utworzyło obiekt tymczasowy (odpowiedniofoo4
ifoo6
), którego Destruktor został wywołany natychmiast po zakończeniu wstawiania. Dodatkowo, wewnętrzneFoo
unordered_map (które są foos 5 i 7) również miały wywoływane destruktory, gdy unordered_map został zniszczony.B. Aby wstawić
foo2
, my zamiast tego najpierw utworzono nie-tymczasową parę (foo8
), która wywołała Konstruktor kopiującyFoo
, a następnie wstawiono go, co spowodowało, że unordered_map ponownie wywołał Konstruktor kopiujący, aby utworzyć wewnętrzną kopię (foo9
). Podobnie jak w przypadku foos 0 i 1, efektem końcowym były dwa wywołania destruktora dla tego Wstawienia, z tą tylko różnicą, że Destruktorfoo8
został wywołany dopiero, gdy osiągnęliśmy koniecmain()
. Emplacing
foo3
spowodował tylko 1 wywołanie konstruktora Kopiuj/przenieś (tworzeniefoo10
wewnętrznie) i tylko 1 wywołanie destruktoraFoo
. (Wrócę do tego później).Dla
foo11
, przekazaliśmy bezpośrednio liczbę całkowitą 11 tak, żeunordered_map
wywoła konstruktorFoo(int)
. W przeciwieństwie do funkcji w (3), nie potrzebowaliśmy nawet jakiegoś wcześniej wychodzącego obiektu foo, aby to zrobić.
To pokazuje, jaka jest główna różnica między insert()
a emplace()
:
insert()
prawie zawsze wymaga budowy lub istnienie jakiegośFoo
obiektu w zakresiemain()
(po którym następuje kopiowanie lub przenoszenie), jeśli używa sięemplace()
, to każde wywołanie konstruktora jest wykonywane całkowicie wewnętrznie w zakresieunordered_map
(tzn. wewnątrz zakresu definicji metodyemplace()
). Argument(y) dla klucza, który przekazujeszemplace()
, jest bezpośrednio przekazywany do wywołania konstruktoraFoo
wewnątrzunordered_map
(Opcjonalne dodatkowe szczegóły: jeśli nowo zbudowany obiekt jest natychmiast wbudowany w jedną ze zmiennych składowychunordered_map
, tak aby żaden Destruktor nie został wywołane, gdy wykonanie opuszczaemplace()
i nie są wywoływane konstruktory move lub copy).
Uwaga: przyczyna prawie w prawie zawsze jest wyjaśniona w I) poniżej.
- continued: powód, dla którego wywołanie
umap.emplace(foo3, d)
wywołałoFoo
's non-const copy constructor jest następujący: ponieważ używamyemplace()
, kompilator wie, żefoo3
(obiekt non-const {6]}) ma być argumentem do jakiegoś konstruktoraFoo
. W tym przypadku najbardziej pasujeFoo
konstruktor jest konstrukcją non-const copyFoo(Foo& f2)
. To dlategoumap.emplace(foo3, d)
nazywa Konstruktor kopiujący, podczas gdyumap.emplace(11, d)
Nie.
Epilog:
I. zauważ, że jedno przeciążenie insert()
jest w rzeczywistości równoważne emplace()
. Jak opisano w tym cppreference.com strona , przeciążenie template<class P> std::pair<iterator, bool> insert(P&& value)
(czyli przeciążenie (2) z insert()
na tej cppreference.com strona) jest odpowiednikiem emplace(std::forward<P>(value))
.
II. dokąd się stąd udać?
A. Pobaw się z powyższym źródłem kod i dokumentacja badań dla insert()
(np. tutaj) i emplace()
(np. tutaj), które można znaleźć online. Jeśli używasz IDE, takiego jak eclipse lub NetBeans, możesz łatwo uzyskać IDE, aby powiedzieć, które przeciążenie insert()
lub emplace()
jest wywoływane(w eclipse, po prostu trzymaj kursor myszy na stałym wywołaniu funkcji przez sekundę). Oto kilka kodów do wypróbowania:
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 to 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 cals
// 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}}));
Wkrótce zobaczysz, że przeciążenie std::pair
konstruktora (zobacz Referencja ) może mieć istotny wpływ na to, ile obiektów zostanie skopiowanych, przeniesionych, utworzonych i/lub zniszczonych oraz kiedy to nastąpi.
B. Zobacz, co się dzieje, gdy używasz innej klasy kontenera (np. std::set
lub std::unordered_multiset
) zamiast std::unordered_map
.
C. Teraz użyj Goo
obiektu (tylko zmienioną kopię Foo
) zamiast int
jako typu zakresu w unordered_map
(tzn. użyj unordered_map<Foo, Goo>
zamiast unordered_map<Foo, int>
) i zobacz, ile i które konstruktory Goo
są dzwoniłem. (Spoiler: jest efekt, ale nie jest zbyt dramatyczny.)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-25 17:47:02