Dlaczego miałbym kiedykolwiek używać push_back zamiast laborace_back?


232

Wektory C ++ 11 mają nową funkcję emplace_back. W przeciwieństwie do tego push_back, który polega na optymalizacji kompilatora w celu uniknięcia kopiowania, emplace_backwykorzystuje doskonałe przekazywanie, aby wysyłać argumenty bezpośrednio do konstruktora w celu utworzenia obiektu w miejscu. Wydaje mi się, że emplace_backwszystko push_backrobi, ale czasami zrobi to lepiej (ale nigdy gorzej).

Z jakiego powodu muszę skorzystać push_back?

Odpowiedzi:


162

Zastanawiałem się nad tym pytaniem przez ostatnie cztery lata. Doszedłem do wniosku, że w większości wyjaśnień dotyczących push_backkontra emplace_backbrakuje 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_backvs. emplace_backo 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_backlub 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, vpodczas gdy emplace_backprzekaż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_backwywołują dowolny konstruktor, podczas gdy bardziej ostrożne push_backbędą wywoływać tylko konstruktory, które są niejawne. Domniemane konstruktory powinny być bezpieczne. Jeśli możesz w sposób niejawny skonstruować Uz a T, mówisz, że Umoże przechowywać wszystkie informacje Tbez żadnych strat. Przechodzenie jest bezpieczne w prawie każdej sytuacji Ti nikt nie będzie miał nic przeciwko, jeśli zrobisz to Uzamiast. Dobrym przykładem niejawnego konstruktora jest konwersja z std::uint32_tna std::uint64_t. Zły przykład niejawna konwersja jest doubledo 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_backmożna wywoływać jawne konstruktory, przekazanie wskaźnika nie będącego właścicielem kompiluje się dobrze. Jednak gdy vwykracza poza zakres, destruktor spróbuje wywołać deleteten wskaźnik, który nie został przydzielony, newponieważ 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_backna 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.


Byłoby pomocne, gdybyś mógł opracować dokładnie, co dokładnie robi miejsce w twoim przykładzie i dlaczego jest to błędne.
eddi,

4
@eddi: Dodałem sekcję wyjaśniającą to: std::unique_ptr<T>ma jawny konstruktor T *. Ponieważ emplace_backmożna wywoływać jawne konstruktory, przekazanie wskaźnika nie będącego właścicielem kompiluje się dobrze. Jednak gdy vwykracza poza zakres, destruktor spróbuje wywołać deleteten wskaźnik, który nie został przydzielony, newponieważ jest to tylko obiekt stosu. Prowadzi to do nieokreślonego zachowania.
David Stone,

Dzięki za opublikowanie tego. Nie wiedziałem o tym, kiedy pisałem swoją odpowiedź, ale teraz żałuję, że sam jej nie napisałem, kiedy się tego nauczyłem :) Naprawdę chcę spoliczkować ludzi, którzy przełączają się na nowe funkcje, aby zrobić najmodniejszą rzecz, jaką mogą znaleźć . Ludzie, ludzie używali C ++ również przed C ++ 11, i nie wszystko w tym było problematyczne. Jeśli nie wiesz, dlaczego używasz funkcji, nie używaj jej . Tak się cieszę, że to opublikowałeś i mam nadzieję, że zyska dużo więcej głosów pozytywnych, więc przekroczy moje. +1
użytkownik541686,

1
@CaptainJacksparrow: Wygląda na to, że mówię wprost i wprost tam, gdzie mam na myśli. Którą część pomyliłeś?
David Stone

4
@CaptainJacksparrow: explicitKonstruktor to konstruktor, do którego explicitzastosowano słowo kluczowe . Konstruktorem „niejawnym” jest każdy konstruktor, który nie ma tego słowa kluczowego. W przypadku std::unique_ptrkonstruktora z T *, implementator std::unique_ptrnapisał 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ło push_back, zamiast wywoływać tego konstruktora, polegałby na niejawnej konwersji, która może wywoływać tylko niejawne konstruktory.
David Stone

117

push_backzawsze pozwala na użycie jednolitej inicjalizacji, co bardzo lubię. Na przykład:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

Z drugiej strony v.emplace_back({ 42, 121 });nie będzie działać.


58
Należy zauważyć, że dotyczy to tylko inicjalizacji zbiorczej i inicjalizacji listy inicjalizacyjnej. Jeśli użyjesz {}składni do wywołania rzeczywistego konstruktora, możesz po prostu usunąć je {}i użyć emplace_back.
Nicol Bolas,

Głupi czas na pytanie: więc w ogóle nie można używać w ramach wektorów structsback? A może po prostu nie dla tego stylu przy użyciu dosłownego {42 121]?
Phil H

1
@LucDanton: Jak powiedziałem, dotyczy to tylko inicjalizacji listy agregującej i inicjalizującej. Możesz użyć składni do wywołania rzeczywistych konstruktorów. Możesz podać konstruktor, który przyjmuje 2 liczby całkowite, a ten konstruktor zostałby wywołany przy użyciu składni. Chodzi o to, że jeśli próbujesz wywołać konstruktor, byłoby lepiej, ponieważ wywołuje on konstruktor w miejscu. Dlatego nie wymaga kopiowania tego typu. {}aggregate{}emplace_back
Nicol Bolas,

11
Zostało to uznane za wadę standardu i zostało rozwiązane. Zobacz cplusplus.github.io/LWG/lwg-active.html#2089
David Stone

3
@DavidStone Gdyby został rozwiązany, nadal nie byłby na liście „aktywnej” ... nie? Wydaje się, że pozostaje nierozwiązanym problemem. Najnowsza aktualizacja, zatytułowana „ [2018-08-23 Przetwarzanie problemów Batavii]] , mówi, że„ P0960 (obecnie w locie) powinien to rozwiązać. ”I nadal nie mogę skompilować kodu, który próbuje emplaceagregować 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.
underscore_d

80

Kompatybilność wsteczna z kompilatorami wcześniejszymi niż C ++ 11.


21
To wydaje się być przekleństwem C ++. Dostajemy mnóstwo fajnych funkcji z każdym nowym wydaniem, ale wiele firm albo utknęło przy użyciu starej wersji ze względu na kompatybilność, albo zniechęcało (jeśli nie nie zezwala) na korzystanie z niektórych funkcji.
Dan Albert

6
@Mehrdad: Po co zadowalać się wystarczającym, skoro możesz mieć coś wielkiego? Na pewno nie chciałbym programować w blubie , nawet jeśli to wystarczyło. Nie oznacza to, że dotyczy to w szczególności tego przykładu, ale ponieważ ktoś, kto spędza większość czasu na programowaniu w C89 ze względu na kompatybilność, jest to z pewnością prawdziwy problem.
Dan Albert,

3
Nie sądzę, że to naprawdę odpowiedź na pytanie. Dla mnie prosi o przypadki użycia, w których push_backlepiej.
Pan Boy

3
@ Mr.Boy: Lepiej jest, jeśli chcesz być kompatybilny wstecz z kompilatorami wcześniejszymi niż C ++ 11. Czy było to niejasne w mojej odpowiedzi?
user541686,

6
Zwróciło to na siebie większą uwagę, niż się spodziewałem, więc dla was wszystkich czytających to: nieemplace_back jest to „świetna” wersja . Jest to potencjalnie niebezpieczna wersja. Przeczytaj pozostałe odpowiedzi. push_back
user541686,

68

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.

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

W jednym kompilatorze v zawiera wartości 123 i 21 zamiast oczekiwanych 123 i 123. Wynika to z faktu, że drugie wywołanie emplace_backpowoduje zmianę rozmiaru, w którym punkt v[0]staje się nieważny.

Działająca implementacja powyższego kodu użyłaby push_back()zamiast emplace_back()następujących:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

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.


Czekać. To wydaje się być błędem. Jak może push_backbyć inaczej w tym przypadku?
balki

12
Wywołanie do Situace_back () używa doskonałego przekierowania do wykonania konstrukcji w miejscu i jako takie v [0] nie jest oceniane, dopóki po zmianie rozmiaru wektora (w którym punkcie v [0] jest niepoprawny). push_back konstruuje nowy element i kopiuje / przesuwa element zgodnie z potrzebami, a v [0] jest oceniane przed jakąkolwiek realokacją.
Marc

1
@Marc: Standardowo gwarantuje to, że Situace_back działa nawet dla elementów z zakresu.
David Stone

1
@DavidStone: Czy możesz podać odniesienie do tego, w którym standardzie takie zachowanie jest gwarantowane? Tak czy inaczej, Visual Studio 2012 i 2015 wykazują nieprawidłowe zachowanie.
Marc

4
@cameino: Situace_back istnieje w celu opóźnienia oceny jego parametru w celu ograniczenia niepotrzebnego kopiowania. Zachowanie jest niezdefiniowane lub zawiera błąd kompilatora (w oczekiwaniu na analizę standardu). Niedawno uruchomiłem ten sam test przeciwko Visual Studio 2015 i dostałem 123,3 w wersji x64, 123,40 w wersji Win32 i 123, -572662307 w Debug x64 i Debug Win32.
Marc
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.