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.