Почему я должен использовать push_back вместо emplace_back?

С++ 11 векторы имеют новую функцию emplace_back. В отличие от push_back, который полагается на оптимизацию компилятора, чтобы избежать копирования, emplace_back использует совершенную пересылку для отправки аргументов непосредственно конструктору для создания объекта на месте. Мне кажется, что emplace_back делает все push_back, но некоторое время он будет делать это лучше (но не хуже).

Какую причину я должен использовать push_back?

Ответ 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. Вместо этого я замаскировал ошибку и не нашел ее до нескольких месяцев.

Ответ 2

push_back всегда позволяет использовать единую инициализацию, которую я очень люблю. Например:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

С другой стороны, v.emplace_back({ 42, 121 }); не будет работать.

Ответ 3

Обратная совместимость с компиляторами pre-С++ 11.

Ответ 4

Некоторые реализации библиотеки emplace_back не ведут себя так, как указано в стандарте С++, включая версию, поставляемую с 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. Это связано с тем, что 2-й вызов emplace_back приводит к изменению размера, при котором точка v[0] становится недействительной.

Рабочая реализация вышеуказанного кода будет использовать push_back() вместо emplace_back() следующим образом:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

Примечание. Использование вектора ints для демонстрационных целей. Я обнаружил эту проблему с гораздо более сложным классом, который включал динамически распределенные переменные-члены, а вызов emplace_back() привел к жесткому сбою.