Zastanawiałem się nad tym pytaniem przez ostatnie cztery lata. Doszedłem do wniosku, że w większości wyjaśnień dotyczących push_back
kontra emplace_back
brakuje pełnego obrazu.
W zeszłym roku wygłosiłem prezentację na C ++ Now o Type Deduction w C ++ 14 . Zaczynam mówić o push_back
vs. emplace_back
o 13:49, ale są przydatne informacje, które dostarczają wcześniej pewnych dowodów potwierdzających.
Prawdziwa pierwotna różnica dotyczy konstruktorów niejawnych i jawnych. Rozważ przypadek, w którym mamy jeden argument, który chcemy przekazać do push_back
lub emplace_back
.
std::vector<T> v;
v.push_back(x);
v.emplace_back(x);
Po tym, jak kompilator optymalizacyjny weźmie to pod uwagę, nie ma różnicy między tymi dwiema instrukcjami pod względem generowanego kodu. Tradycyjna mądrość polega na tym, push_back
że skonstruuje tymczasowy obiekt, do którego zostanie następnie przeniesiony, v
podczas gdy emplace_back
przekaże dalej argument i skonstruuje go bezpośrednio w miejscu, bez kopii lub ruchów. Może to być prawda w oparciu o kod zapisany w standardowych bibliotekach, ale błędnie przyjmuje założenie, że zadaniem kompilatora optymalizacyjnego jest wygenerowanie napisanego kodu. Zadaniem kompilatora optymalizującego jest w rzeczywistości wygenerowanie kodu, który napisalibyście, gdybyście byli ekspertami w zakresie optymalizacji specyficznych dla platformy i nie dbali o łatwość konserwacji, tylko wydajność.
Rzeczywista różnica między tymi dwiema stwierdzeniami polega na tym, że silniejsze emplace_back
wywołują dowolny konstruktor, podczas gdy bardziej ostrożne push_back
będą wywoływać tylko konstruktory, które są niejawne. Domniemane konstruktory powinny być bezpieczne. Jeśli możesz w sposób niejawny skonstruować U
z a T
, mówisz, że U
może przechowywać wszystkie informacje T
bez żadnych strat. Przechodzenie jest bezpieczne w prawie każdej sytuacji T
i nikt nie będzie miał nic przeciwko, jeśli zrobisz to U
zamiast. Dobrym przykładem niejawnego konstruktora jest konwersja z std::uint32_t
na std::uint64_t
. Zły przykład niejawna konwersja jest double
do std::uint8_t
.
Chcemy być ostrożni w naszym programowaniu. Nie chcemy korzystać z zaawansowanych funkcji, ponieważ im bardziej zaawansowana funkcja, tym łatwiej jest przypadkowo zrobić coś niepoprawnego lub nieoczekiwanego. Jeśli zamierzasz wywoływać jawne konstruktory, potrzebujesz mocy emplace_back
. Jeśli chcesz wywoływać tylko niejawne konstruktory, trzymaj się bezpieczeństwa push_back
.
Przykład
std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile
std::unique_ptr<T>
ma jawny konstruktor z T *
. Ponieważ emplace_back
można wywoływać jawne konstruktory, przekazanie wskaźnika nie będącego właścicielem kompiluje się dobrze. Jednak gdy v
wykracza poza zakres, destruktor spróbuje wywołać delete
ten wskaźnik, który nie został przydzielony, new
ponieważ jest to tylko obiekt stosu. Prowadzi to do nieokreślonego zachowania.
To nie jest tylko wymyślony kod. To był prawdziwy błąd produkcyjny, z którym się spotkałem. Kod był std::vector<T *>
, ale był właścicielem treści. W ramach migracji do C ++ 11, ja właściwie zmieniło T *
się std::unique_ptr<T>
w celu wskazania, że wektor własność swoją pamięć. Jednak te zmiany oparłem na swoim zrozumieniu w 2012 roku, podczas którego pomyślałem: „Situace_back robi wszystko, co może zrobić push_back i więcej, więc dlaczego miałbym kiedykolwiek używać push_back?”, Więc zmieniłem również push_back
na emplace_back
.
Gdybym zamiast tego zostawił kod jako bezpieczniejszy push_back
, natychmiast złapałbym ten długotrwały błąd i byłby postrzegany jako sukces aktualizacji do C ++ 11. Zamiast tego zamaskowałem błąd i znalazłem go dopiero kilka miesięcy później.
std::unique_ptr<T>
ma jawny konstruktorT *
. Ponieważemplace_back
można wywoływać jawne konstruktory, przekazanie wskaźnika nie będącego właścicielem kompiluje się dobrze. Jednak gdyv
wykracza poza zakres, destruktor spróbuje wywołaćdelete
ten wskaźnik, który nie został przydzielony,new
ponieważ jest to tylko obiekt stosu. Prowadzi to do nieokreślonego zachowania.explicit
Konstruktor to konstruktor, do któregoexplicit
zastosowano słowo kluczowe . Konstruktorem „niejawnym” jest każdy konstruktor, który nie ma tego słowa kluczowego. W przypadkustd::unique_ptr
konstruktora zT *
, implementatorstd::unique_ptr
napisał ten konstruktor, ale problem polega na tym, że użytkownik tego typu zadzwoniłemplace_back
, co wywołało tego jawnego konstruktora. Gdyby tak byłopush_back
, zamiast wywoływać tego konstruktora, polegałby na niejawnej konwersji, która może wywoływać tylko niejawne konstruktory.push_back
zawsze pozwala na użycie jednolitej inicjalizacji, co bardzo lubię. Na przykład:Z drugiej strony
v.emplace_back({ 42, 121 });
nie będzie działać.źródło
{}
składni do wywołania rzeczywistego konstruktora, możesz po prostu usunąć je{}
i użyćemplace_back
.{}
aggregate
{}
emplace_back
emplace
agregować bez wyraźnego napisania konstruktora . W tym momencie nie jest również jasne, czy będzie traktowane jako wada i tym samym kwalifikuje się do backportowania, czy też użytkownicy C ++ <20 pozostaną SoL.Kompatybilność wsteczna z kompilatorami wcześniejszymi niż C ++ 11.
źródło
push_back
lepiej.emplace_back
jest to „świetna” wersja . Jest to potencjalnie niebezpieczna wersja. Przeczytaj pozostałe odpowiedzi.push_back
Niektóre implementacje biblioteczne programu Situace_back nie zachowują się tak, jak określono w standardzie C ++, w tym wersja dostarczana z Visual Studio 2012, 2013 i 2015.
Aby uwzględnić znane błędy kompilatora, wolisz używać,
std::vector::push_back()
jeśli parametry odnoszą się do iteratorów lub innych obiektów, które będą niepoprawne po wywołaniu.W jednym kompilatorze v zawiera wartości 123 i 21 zamiast oczekiwanych 123 i 123. Wynika to z faktu, że drugie wywołanie
emplace_back
powoduje zmianę rozmiaru, w którym punktv[0]
staje się nieważny.Działająca implementacja powyższego kodu użyłaby
push_back()
zamiastemplace_back()
następujących:Uwaga: użycie wektora liczb całkowitych służy do celów demonstracyjnych. Odkryłem ten problem ze znacznie bardziej złożoną klasą, która zawierała dynamicznie alokowane zmienne składowe i wezwanie do
emplace_back()
spowodowania ciężkiej awarii.źródło
push_back
być inaczej w tym przypadku?