За последние четыре года я много думал об этом. Я пришел к выводу, что большинство объяснений по отношению к push_back
против emplace_back
пропускают полную картину.
В прошлом году я выступил с докладом на C ++. Теперь о типе дедукции в 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
вызовут только неявные конструкторы. Неявные конструкторы должны быть безопасными. Если вы можете неявно построить a U
из a 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. Вместо этого я замаскировал ошибку и не нашел ее несколько месяцев спустя.