Я много думал над этим вопросом за последние четыре года. Я пришел к выводу, что в большинстве объяснений push_back
и emplace_back
отсутствует полная картина.
В прошлом году я выступал на C ++ Now по теме Выведение типов в C ++ 14 а>. Я начинаю говорить о push_back
vs. 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. Вместо этого я замаскировал ошибку и обнаружил ее только через несколько месяцев.
person
David Stone
schedule
28.04.2016