Ответ 1
Я думал об этом вопросе совсем немного за последние четыре года. Я пришел к выводу, что большинство объяснений о push_back
vs. emplace_back
пропускают полную картину.
В прошлом году я представил презентацию на С++ Now Type Deduction в С++ 14. Я начинаю говорить о push_back
vs. emplace_back
в 13:49, но есть полезная информация, которая предоставляет некоторые подтверждающие доказательства до этого.
Реальное основное различие связано с неявным конструктором vs. Рассмотрим случай, когда у нас есть один аргумент, который мы хотим передать в 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
будет вызывать только неявные конструкторы. Неявные конструкторы должны быть безопасными. Если вы можете неявно построить a U
из T
, вы говорите, что U
может хранить всю информацию в T
без потерь. В любой ситуации безопасно пропускать T
, и никто не возражает, если вы сделаете это U
. Хорошим примером неявного конструктора является преобразование из std::uint32_t
в std::uint64_t
. Неверным примером неявного преобразования является double
to 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
, потому что это всего лишь объект стека. Это приводит к поведению undefined.
Это не просто придуманный код. Это была настоящая ошибка в производстве, с которой я столкнулся. Код был std::vector<T *>
, но он владел содержимым. В рамках перехода на С++ 11 я правильно изменил T *
на std::unique_ptr<T>
, указав, что вектор принадлежит своей памяти. Тем не менее, я основывал эти изменения на своем понимании в 2012 году, в течение которых я думал, что "emplace_back делает все, что делает push_back, и многое другое, поэтому зачем мне когда-либо использовать push_back?", Поэтому я также изменил push_back
на emplace_back
.
Если бы я вместо этого оставил код как более безопасный push_back
, я бы сразу поймал эту давнюю ошибку, и это было бы оценено как успешное обновление до С++ 11. Вместо этого я замаскировал ошибку и не нашел ее до нескольких месяцев.