Я задумався над цим питанням за останні чотири роки. Я дійшов висновку, що більшість пояснень щодо push_back
проти emplace_back
пропускають повну картину.
Минулого року я виступив з доповіддю на C ++ Now про вирахування типу в C ++ 14 . Я починаю говорити push_back
проти emplace_back
13: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. Натомість я замаскував помилку і не знайшов її лише місяцями пізніше.