Чому я коли-небудь використовувати push_back замість emplace_back?


232

C ++ 11 векторів мають нову функцію emplace_back. На відміну від того push_back, що покладається на оптимізацію компілятора, щоб уникнути копій, emplace_backвикористовує ідеальну переадресацію для передачі аргументів безпосередньо конструктору для створення об’єкта на місці. Мені здається, що emplace_backвсе push_backможе зробити, але якийсь час це зробить краще (але ніколи не гірше).

Яку причину мені потрібно використовувати push_back?

Відповіді:


162

Я задумався над цим питанням за останні чотири роки. Я дійшов висновку, що більшість пояснень щодо push_backпроти emplace_backпропускають повну картину.

Минулого року я виступив з доповіддю на C ++ Now про вирахування типу в C ++ 14 . Я починаю говорити push_backпроти emplace_back13:49, але є корисна інформація, яка дає певні підтвердження до цього.

Справжня первинна відмінність стосується неявних та явних конструкторів. Розглянемо випадок, коли у нас є єдиний аргумент, який ми хочемо передати push_backабо emplace_back.

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);

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

Фактична різниця між цими двома твердженнями полягає в тому, що чим потужніший emplace_backбуде викликати будь-який тип конструктора там, тоді як більш обережні push_backбудуть називати лише конструктори, які неявні. Неявні конструктори повинні бути безпечними. Якщо ви можете неявно побудувати а Uз а T, ви говорите, що Uможете зберігати всю інформацію в програмі Tбез втрат. Це в безпеці практично в будь-якій ситуації пройти Tі ніхто не заперечує, якщо ви зробите це Uзамість цього. Хорошим прикладом неявного конструктора є перетворення з std::uint32_tна std::uint64_t. Поганий приклад неявного перетворення - doubleце std::uint8_t.

Ми хочемо бути обережними у нашому програмуванні. Ми не хочемо використовувати потужні функції, оскільки чим потужніша функція, тим легше випадково зробити щось неправильне або несподіване. Якщо ви маєте намір викликати явні конструктори, тоді вам потрібна сила emplace_back. Якщо ви хочете закликати лише неявні конструктори, дотримуйтесь безпеки push_back.

Приклад

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>має явний конструктор від T *. Оскільки emplace_backможна викликати явні конструктори, передача невласнивого покажчика компілюється просто чудово. Однак, коли vвиходить за межі, деструктор спробує зателефонувати deleteза тим вказівником, який не був виділений, newоскільки це просто об'єкт стека. Це призводить до невизначеної поведінки.

Це не просто придуманий код. Це була справжня помилка виробництва, з якою я стикався. Код був std::vector<T *>, але він володів вмістом. В рамках переходу на C ++ 11, я правильно змінений T *на , std::unique_ptr<T>щоб вказати , що вектор належить його пам'яті. Однак я базував ці зміни на своєму розумінні в 2012 році, під час якого я думав, що "emplace_back робить все, що push_back може зробити і багато іншого, то чому я коли-небудь використовувати push_back?", Тому я також змінив push_backна emplace_back.

Якби я замість цього залишив код як безпечніший push_back, я би негайно зловив цю давню помилку, і це вважалося б успіхом оновлення до C ++ 11. Натомість я замаскував помилку і не знайшов її лише місяцями пізніше.


Це допоможе, якщо ви зможете детальніше розібратися, що саме робить ваш приклад, і чому це неправильно.
Едді

4
@eddi: Я додав розділ, що пояснює це: std::unique_ptr<T>має явний конструктор від T *. Оскільки emplace_backможна викликати явні конструктори, передача невласнивого покажчика компілюється просто чудово. Однак, коли vвиходить за межі, деструктор спробує зателефонувати deleteза тим вказівником, який не був виділений, newоскільки це просто об'єкт стека. Це призводить до невизначеної поведінки.
Девід Стоун

Дякуємо, що опублікували це. Я не знав про це, коли писав свою відповідь, але тепер я б хотів, щоб я написав це сам, коли я дізнався його згодом :) Я дуже хочу ляпати людей, які переходять на нові функції, просто щоб зробити найшвидшу річ, яку вони можуть знайти . Хлопці, люди також використовували C ++ і раніше, ніж C ++ 11, і не все про це було проблематичним. Якщо ви не знаєте, чому ви використовуєте функцію, не використовуйте її . Тож радий, що ви опублікували це, і я сподіваюся, що це отримає набагато більше оновлень, тому воно виходить вище мого. +1
користувач541686

1
@CaptainJacksparrow: Схоже, я кажу неявно і явно там, де я їх маю на увазі. Яку частину ви плутали?
Девід Стоун

4
@CaptainJacksparrow: explicitКонструктор - це конструктор, до якого explicitзастосовано ключове слово . "Неявний" конструктор - це будь-який конструктор, у якого немає цього ключового слова. Що стосується конструктора std::unique_ptr's T *, то реалізатор std::unique_ptrнаписав цей конструктор, але проблема полягає в тому, що користувач цього типу викликав emplace_back, який назвав цей явний конструктор. Якби це було push_back, замість виклику цього конструктора, він би покладався на неявне перетворення, яке може викликати лише неявні конструктори.
Девід Стоун

117

push_backзавжди дозволяє використовувати рівномірну ініціалізацію, що мені дуже подобається. Наприклад:

struct aggregate {
    int foo;
    int bar;
};

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

З іншого боку, v.emplace_back({ 42, 121 });не вийде.


58
Зауважте, що це стосується лише сукупної ініціалізації та ініціалізації списку ініціалізаторів. Якщо ви використовуєте {}синтаксис для виклику фактичного конструктора, ви можете просто видалити {}і використовувати emplace_back.
Нікол Болас

Тупий час запитання: тож emplace_back взагалі не можна використовувати для векторів структур? Або просто не для цього стилю, використовуючи буквальний {42,121}?
Філ Х

1
@LucDanton: Як я вже сказав, це стосується лише ініціалізації списку агрегатів та ініціалізаторів. Ви можете використовувати синтаксис для виклику фактичних конструкторів. Ви можете дати конструктору, який бере 2 цілих числа, і цей конструктор буде викликаний при використанні синтаксису. Справа в тому, що якщо ви намагаєтесь викликати конструктор, було б краще, оскільки він викликає конструктор на місці. А тому не вимагає копіювання цього типу. {}aggregate{}emplace_back
Нікол Болас

11
Це було розглянуто як дефект у стандарті та було вирішено. Дивіться cplusplus.github.io/LWG/lwg-active.html#2089
Девід Стоун

3
@DavidStone Якби це було вирішено, воно все ще не було б у списку "активних" ... ні? Здається, залишається невирішеною проблемою. Останнє оновлення під назвою " [2018-08-23 Виправлення проблем Batavia] " говорить про те, що " P0960 (наразі знаходиться в польоті) повинен це вирішити ". І я досі не можу скласти код, який намагається emplaceагрегувати, не чітко записуючи конструктор котлоагрегату . На даний момент також незрозуміло, чи буде він розглядатися як дефект і таким чином придатний для резервування, чи користувачі C ++ <20 залишаться SoL.
підкреслити_6

80

Зворотна сумісність із компіляторами, що передували C ++ 11.


21
Це, здається, є прокляттям C ++. З кожною новою версією ми отримуємо багато цікавих функцій, але багато компаній або затримуються, використовуючи стару версію заради сумісності, або відмовляють (якщо не забороняють) використовувати певні функції.
Ден Альберт

6
@Mehrdad: Чому домовлятися про достатнє, коли ти можеш чудово? Я впевнений, що не хотів би програмувати в клубі , навіть якщо це було достатньо. Не кажучи, що це стосується конкретного прикладу, але як хтось, хто проводить більшу частину свого часу програмування на C89 заради сумісності, це, безумовно, справжня проблема.
Дан Альберт

3
Я не думаю, що це насправді відповідь на питання. Для мене він просить використовувати випадки, коли push_backкраще.
Містер Хлопчик

3
@ Mr.Boy: Бажано, коли ви хочете бути зворотно сумісними з компіляторами pre-C ++ 11. Це було незрозуміло в моїй відповіді?
користувач541686

6
Це отримало набагато більше уваги , ніж я очікувані, так і для всіх , хто читає це: emplace_backце НЕ «велика» версія push_back. Це потенційно небезпечна його версія. Прочитайте інші відповіді.
користувач541686

68

Деякі реалізації бібліотеки emplace_back не поводяться так, як зазначено в стандарті C ++, включаючи версію, що постачається з Visual Studio 2012, 2013 та 2015.

Щоб розмістити відомі помилки компілятора, віддайте перевагу використанню, std::vector::push_back()якщо параметри посилаються на ітератори чи інші об'єкти, які будуть недійсними після виклику.

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

На одному компіляторі v містить значення 123 і 21 замість очікуваних 123 і 123. Це пов'язано з тим, що другий виклик emplace_backпризводить до зміни розміру, в який момент v[0]стає недійсним.

Робоча реалізація вищевказаного коду використовує push_back()замість emplace_back()наступного:

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

Примітка. Використання вектора ints призначено для демонстрації. Я виявив цю проблему набагато складнішим класом, який включав динамічно розподілені змінні члена та виклик, що emplace_back()призвів до важкої аварії.


Зачекайте. Здається, це помилка. Як можна push_backвідрізнятись у цьому випадку?
балки

12
Виклик emplace_back () використовує ідеальну переадресацію для виконання побудови на місці і як такий v [0] не оцінюється, поки не буде змінено вектор (в якому точка v [0] недійсна). push_back конструює новий елемент і копіює / переміщує елемент за потребою і v [0] оцінюється перед будь-яким перерозподілом.
Марк

1
@Marc: Стандартом гарантується, що emplace_back працює навіть для елементів усередині діапазону.
Девід Стоун

1
@DavidStone: Чи не могли б Ви вказати, де в стандарті гарантується така поведінка? Так чи інакше, Visual Studio 2012 та 2015 виявляють неправильну поведінку.
Марк

4
@cameino: emplace_back існує, щоб затримати оцінку свого параметра, щоб зменшити непотрібне копіювання. Поведінка або не визначена, або помилка компілятора (очікує на аналіз стандарту). Нещодавно я провів той же тест проти Visual Studio 2015 і отримав 123,3 під Release x64, 123,40 під Release Win32 та 123, -572662307 під Debug x64 та Debug Win32.
Марк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.