Почему я бы использовал push_back вместо emplace_back?
С ++11 векторов имеют новую функцию emplace_back
, В отличие от push_back
, который опирается на оптимизацию компилятора, чтобы избежать копий, emplace_back
использует совершенную пересылку для отправки аргументов непосредственно в конструктор для создания объекта на месте. Мне кажется, что emplace_back
делает все push_back
можно сделать, но иногда это будет лучше (но никогда не хуже).
По какой причине я должен использовать push_back
?
6 ответов
За последние четыре года я много думал об этом. Я пришел к выводу, что большинство объяснений о 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
будет вызывать только неявные конструкторы. Неявные конструкторы должны быть безопасными. Если вы можете неявно построить 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. Вместо этого я замаскировал ошибку и не нашел ее до нескольких месяцев спустя.
push_back
всегда позволяет использовать унифицированную инициализацию, что мне очень нравится. Например:
struct aggregate {
int foo;
int bar;
};
std::vector<aggregate> v;
v.push_back({ 42, 121 });
С другой стороны, v.emplace_back({ 42, 121 });
не будет работать.
Некоторые реализации библиотеки emplace_back не ведут себя так, как указано в стандарте C++, включая версию, поставляемую с Visual Studio 2012, 2013 и 2015.
Чтобы учесть известные ошибки компилятора, предпочитайте использоватьstd::vector::push_back()
если параметры ссылаются на итераторы или другие объекты, которые будут недействительными после вызова.
std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers
В одном компиляторе v содержит значения 123 и 21 вместо ожидаемых 123 и 123. Это связано с тем, что второй вызов emplace_back
приводит к изменению размера, в какой момент v[0]
становится недействительным.
Рабочая реализация приведенного выше кода будет использовать push_back()
вместо emplace_back()
следующее:
std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);
Примечание: использование вектора целых для демонстрационных целей. Я обнаружил эту проблему с гораздо более сложным классом, который включал динамически распределяемые переменные-члены и вызов emplace_back()
привело к тяжелой аварии.
Использоватьpush_back
только для примитивных/встроенных типов или необработанных указателей. В противном случае используйтеemplace_back
.
Рассмотрим, что происходит в Visual Studio 2019 с компилятором C++-17. У нас есть emplace_back в функции с правильно настроенными аргументами. Затем кто-то изменяет параметры конструктора, вызываемого emplace_back. В VS нет никаких предупреждений, код также компилируется нормально, а затем вылетает во время выполнения. После этого я удалил все emplace_back из кодовой базы.