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