На ревью утверждают, что замена push back на emplace back всегда устраняет копирование элемента. Верно ли это?

На ревью утверждают, что замена push_back на emplace_back всегда устраняет копирование элемента. Верно ли это?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет. emplace_back конструирует новый элемент непосредственно в памяти контейнера, но не устраняет копирование или перемещение во всех случаях. Если передать уже созданный объект, потребуется его копирование или перемещение, а при перераспределении памяти std::vector могут перемещаться или копироваться и ранее добавленные элементы.

Исторический контекст

emplace_back появился вместе с механизмами перемещения и perfect forwarding в C++11. Его задача — дать контейнеру возможность вызвать конструктор элемента прямо в выделенной памяти, не создавая отдельный временный объект для последующего добавления.

Ранее типичный вызов push_back принимал готовый объект. Для добавления элемента, создаваемого из нескольких аргументов, это могло приводить к созданию временного объекта и его последующему перемещению или копированию.

Постановка проблемы

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

Нужно различать два случая: создание элемента из аргументов конструктора и добавление уже существующего объекта. Кроме того, стоимость операции определяется не только способом добавления, но и текущей емкостью контейнера, конструкторами типа и их свойствами исключений.

Подробное решение

При вызове emplace_back аргументы передаются конструктору элемента, который создается сразу в конце контейнера. При вызове push_back обычно передается уже готовый объект, поэтому контейнер помещает в себя его копию или перемещенную версию.

#include <vector> struct Item { explicit Item(int value) : value(value) {} int value; }; int main() { std::vector<Item> v; v.emplace_back(42); // Item создаётся сразу в vector Item item(7); v.emplace_back(item); // копирование item v.push_back(Item(9)); // временный Item перемещается или копируется }

В первом случае emplace_back действительно позволяет избежать отдельного временного объекта. Во втором контейнер получает существующий объект item, поэтому должен скопировать или переместить его независимо от названия метода.

При расширении std::vector может потребоваться выделить новый буфер и перенести туда все элементы. Если перемещающий конструктор элемента не является noexcept или недоступен, реализация может предпочесть копирование ради сохранения гарантий исключений. Поэтому emplace_back не гарантирует отсутствие операций над уже существующими элементами.

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

Ситуация из практики

В сервисе элементы дорогостоящего типа добавлялись в вектор. Разработчик заменил все вызовы push_back на emplace_back и ожидал заметного ускорения. Измерения показали, что большая часть времени уходила на расширение вектора, а не на создание временных объектов.

Рассматривались два варианта. Механическая замена методов почти не меняла число перемещений при перераспределении и ухудшала читаемость кода для уже созданных объектов. Предварительный вызов reserve уменьшал число перераспределений, но требовал реалистичной оценки количества элементов и увеличивал заранее выделенный объем памяти.

Выбрали emplace_back для построения элементов из аргументов, push_back для уже существующих объектов и предварительный reserve там, где размер входных данных был известен. Это устранило лишние перераспределения, сохранило ясность кода и не создало ложной гарантии отсутствия копирований.

Что кандидаты часто упускают

  1. Дополнительный вопрос: Что произойдет, если передать в emplace_back объект того же типа, который уже существует?

Ответ: Будет выбран конструктор копирования или перемещения, принимающий этот объект. Непосредственная конструкция нового элемента из аргументов не означает, что существующий объект можно поместить в контейнер без операции над ним.

  1. Дополнительный вопрос: Почему emplace_back не гарантирует отсутствие перемещений при росте vector?

Ответ: При нехватке емкости вектор выделяет новый буфер и переносит туда уже существующие элементы. Новые элементы могут конструироваться на месте, но это не отменяет перемещения или копирования старых элементов. Число таких операций можно уменьшить с помощью reserve, если требуемый размер известен заранее.

  1. Дополнительный вопрос: Почему push_back иногда выбирают вместо emplace_back даже при возможности передать аргументы конструктора?

Ответ: push_back явно показывает добавление готового объекта и может быть понятнее при работе с уже созданным значением. Кроме того, чрезмерное использование emplace_back иногда приводит к неожиданному выбору конструктора или неявному преобразованию аргументов. Выбор должен определяться смыслом операции, а не предположением, что emplace_back всегда быстрее.