push_back vs emplace_back: в чем разница и что выбрать в C++?
Основное различие: конструирование на месте vs копирование/перемещение
Ключевое отличие между push_back и emplace_back — в том, как создается элемент в контейнере. push_back принимает уже готовый объект (по значению, ссылке или rvalue-ссылке) и копирует/перемещает его в память вектора. emplace_back же конструирует объект непосредственно в памяти вектора, используя переданные аргументы для вызова конструктора.
Рассмотрим детально ваш пример:
// push_back: создается временный объект Person, затем перемещается или копируется
people.push_back(Person("Alice", 30));
// emplace_back: Person конструируется прямо в памяти вектора
people.emplace_back("Bob", 25);
При push_back происходит следующее:
- Создается временный объект
Personв стеке (вызов конструктора). - Вектор выделяет память (если нужно) и вызывает конструктор копирования или перемещения для переноса объекта.
- Временный объект уничтожается.
При emplace_back:
- Вектор выделяет память (если нужно).
- Вызывается конструктор
Person(const std::string&, int)напрямую в выделенной памяти. - Никакого временного объекта не создается.
Когда emplace_back действительно эффективнее?
Основной выигрыш — избежание лишнего копирования/перемещения. Это особенно заметно для:
- Тяжелых объектов с дорогим копированием (например,
std::vector<std::string>или большие структуры). - Некопируемых типов (например,
std::unique_ptr):push_backпотребует перемещения, аemplace_backсработает напрямую. - Конструкторов с несколькими аргументами, где создание временного объекта выглядит громоздко.
Пример с std::unique_ptr:
std::vector<std::unique_ptr<int>> vec;
// push_back не сработает без std::move
vec.push_back(std::make_unique<int>(42)); // OK, но требует перемещения
vec.emplace_back(std::make_unique<int>(42)); // Тоже OK, но без лишнего перемещения
Когда push_back предпочтительнее?
Несмотря на преимущества emplace_back, есть ситуации, где push_back лучше:
- Читаемость кода:
push_backявно показывает, что добавляется объект. Для простых типов (int, double) разницы нет. - Конструкторы с explicit:
emplace_backможет вызвать неявное преобразование, что приведет к неожиданным результатам. - Работа с initializer_list:
push_back({1, 2, 3})работает, аemplace_back— нет (нужно писатьemplace_back(std::initializer_list<int>{1, 2, 3})).
Пример с explicit:
struct Widget {
explicit Widget(int) {}
};
std::vector<Widget> w;
// w.emplace_back(42); // OK
// w.push_back(42); // Ошибка! Нельзя неявно преобразовать int в Widget
w.push_back(Widget(42)); // OK, явное создание
Связь с move-семантикой
С появлением C++11 и move-семантики разница между push_back и emplace_back стала менее критичной. Если у объекта есть эффективный конструктор перемещения (как у std::string или std::vector), то push_back с временным объектом может быть почти таким же быстрым, как emplace_back. Однако для объектов без move-семантики (или с удаленным конструктором копирования) emplace_back — единственный вариант.
Итоговая рекомендация: используйте emplace_back когда:
- Вы передаете аргументы для конструктора напрямую (особенно для сложных типов).
- Объект не поддерживает копирование или перемещение.
- Вы пишете производительный код и хотите избежать лишних операций.
Используйте push_back когда:
- У вас уже есть готовый объект (например, переменная).
- Код должен быть максимально читаемым и очевидным.
- Вы работаете с initializer_list или explicit-конструкторами.
Дополнение: производительность и подводные камни
Хотя emplace_back часто быстрее, есть нюансы, которые стоит учитывать.
1. Проблема с конструкторами, принимающими initializer_list
Если у вашего типа есть конструктор от std::initializer_list, emplace_back может повести себя неожиданно:
std::vector<std::vector<int>> v;
v.emplace_back(10, 20); // Создает vector из 10 элементов со значением 20
v.push_back({10, 20}); // Создает vector из двух элементов: 10 и 20
В первом случае вызывается конструктор vector(10, 20), а не vector({10, 20}). Это частая ошибка.
2. Исключения и гарантии безопасности
emplace_back может оставить контейнер в некорректном состоянии, если конструктор бросит исключение после частичного создания объекта. В C++17 это исправлено, но в более старых стандартах push_back безопаснее.
3. Профилирование — ваш друг
Для простых типов (int, double, указатели) разница между push_back и emplace_back нулевая — компилятор оптимизирует все лишнее. Всегда измеряйте производительность в конкретном контексте, прежде чем делать выводы.
4. Альтернатива: push_back с std::move
Если у вас уже есть объект, который больше не нужен, используйте push_back(std::move(obj)) — это будет так же эффективно, как emplace_back, но код станет понятнее:
Person p("Charlie", 35);
people.push_back(std::move(p)); // Явно показываем намерение переместить
// people.emplace_back(std::move(p)); // Равнозначно, но менее очевидно
В итоге, не стоит слепо заменять все push_back на emplace_back. Думайте о семантике кода и выбирайте инструмент под задачу.
Ответов пока нет.
Войдите чтобы оставить ответ