Avatar ·

push_back vs emplace_back: в чем разница и что выбрать?

Приветствую, коллеги! Я начинающий C++ разработчик и недавно столкнулся с дилеммой: когда использовать push_back, а когда emplace_back? У меня есть простой класс: ```cpp class MyClass { public: MyClass(int x, int y) : x_(x), y_(y) {} private: int x_, y_; }; ``` Я добавляю объекты в вектор двумя способами: ```cpp std::vector vec; // Способ 1 vec.push_back(MyClass(10, 20)); // Способ 2 vec.emplace_back(10, 20); ``` Оба работают, но я слышал, что emplace_back быстрее. Насколько это критично? Есть ли случаи, когда push_back предпочтительнее? Или emplace_back всегда лучше? Особенно меня интересует: что происходит с временными объектами, move-семантикой и копированием? В интернете много противоречивой информации, хочется получить четкий ответ от опытных разработчиков. Спасибо! <<>>

Основное отличие: где создается объект

Главное различие между push_back и emplace_back кроется в месте создания объекта.

  • push_back — принимает уже готовый объект (по значению, ссылке или rvalue-ссылке) и копирует/перемещает его в контейнер.
  • emplace_back — принимает аргументы для конструктора и создает объект непосредственно в памяти вектора, избегая временных объектов.

Детальный разбор производительности

Рассмотрим ваш пример с классом MyClass:

// Вариант с push_back (создание временного объекта)
vec.push_back(MyClass(10, 20)); 
// 1. Создается временный MyClass(10,20) в стеке
// 2. Вызывается конструктор копирования или перемещения для копирования в вектор
// 3. Временный объект уничтожается

// Вариант с emplace_back (конструирование на месте)
vec.emplace_back(10, 20);
// 1. Вектор выделяет память
// 2. Прямо в этой памяти вызывается конструктор MyClass(10,20)
// 3. Никаких временных объектов

emplace_back выигрывает, когда:

  • Объект имеет тяжелый конструктор копирования/перемещения (например, содержит большие массивы или строки).
  • Вы передаете несколько аргументов для конструктора.
  • Работаете с некопируемыми типами (например, std::unique_ptr).

Когда push_back все еще хорош

push_back не устарел и в некоторых случаях даже удобнее:

  • Если у вас уже есть объект (например, переменная), который нужно добавить:
MyClass obj(10, 20);
vec.push_back(obj); // логично: берем существующий объект
vec.emplace_back(obj); // работает, но сбивает с толку
  • При работе с простыми типами (int, double) разница в производительности нулевая.
  • Для кода с move-семантикой:
vec.push_back(std::move(obj)); // явно показываем намерение переместить
vec.emplace_back(std::move(obj)); // менее очевидно

Move-семантика и emplace_back

Важное уточнение: emplace_back не использует move-семантику для переданных аргументов. Он передает их в конструктор как есть (по значению или по ссылке). Если вы хотите переместить объект, используйте push_back с std::move.

Практический совет

Общее правило:

  • Используйте emplace_back, когда создаете объект прямо в вызове с новыми аргументами.
  • Используйте push_back, когда у вас уже есть объект (переменная) или когда хотите явно показать перемещение.

В вашем примере vec.emplace_back(10, 20) — идеальный случай для emplace_back. Но не зацикливайтесь на микрооптимизациях: разница заметна только на тысячах объектов или тяжелых конструкторах.

<<>>

Дополнение: безопасность исключений и читаемость

Есть еще один аспект, который часто упускают — безопасность исключений. emplace_back может быть менее безопасным в некоторых сценариях:

std::vector> vec;
vec.emplace_back(new int(42)); // Потенциальная утечка памяти при исключении

Здесь лучше использовать push_back с std::make_unique:

vec.push_back(std::make_unique(42)); // Безопасно и читаемо

Также помните о читаемости кода. В команде разработчиков push_back часто понятнее новичкам. Не жертвуйте ясностью кода ради незначительного прироста производительности. Для подавляющего большинства случаев разница между push_back и emplace_back не критична — выбирайте то, что делает код понятнее.