Я задумався над цим питанням за останні чотири роки. Я дійшов висновку, що більшість пояснень щодо 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. Натомість я замаскував помилку і не знайшов її лише місяцями пізніше.