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

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему это важно?

Вы когда-нибудь задумывались, почему в C++ существует два способа добавить элемент в конец вектора? Казалось бы, зачем изобретать велосипед, если есть проверенный push_back? Ответ кроется в производительности и гибкости современного C++.

На Stack Overflow этот вопрос собрал тысячи просмотров и сотни комментариев. И это не случайно: понимание разницы между push_back и emplace_back — это не просто академическое знание, а практический навык, который может ускорить ваш код в 2-3 раза. Давайте разберемся раз и навсегда.

Базовое отличие: что говорят документации?

Начнем с простого. Оба метода добавляют элемент в конец контейнера (вектора, списка, очереди). Но делают они это по-разному:

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

Звучит абстрактно? Давайте посмотрим на код.

Практический пример: создаем класс

Представим, что у нас есть простой класс Person:

#include <iostream>
#include <vector>
#include <string>

class Person {
public:
    std::string name;
    int age;

    // Конструктор
    Person(const std::string& n, int a) 
        : name(n), age(a) {
        std::cout << "Конструктор вызван для " << name << std::endl;
    }

    // Конструктор копирования
    Person(const Person& other) 
        : name(other.name), age(other.age) {
        std::cout << "Конструктор копирования для " << name << std::endl;
    }

    // Конструктор перемещения
    Person(Person&& other) noexcept
        : name(std::move(other.name)), age(other.age) {
        std::cout << "Конструктор перемещения для " << name << std::endl;
    }
};

Теперь посмотрим, что произойдет при использовании разных методов.

Вариант 1: push_back с временным объектом

int main() {
    std::vector<Person> people;
    people.push_back(Person("Alice", 25));
    return 0;
}

Вывод:

Конструктор вызван для Alice
Конструктор перемещения для Alice

Что произошло? Сначала создается временный объект Person (первый вызов конструктора). Затем этот объект перемещается в вектор (конструктор перемещения). Два вызова — двойная работа.

Вариант 2: push_back с готовым объектом

int main() {
    std::vector<Person> people;
    Person alice("Alice", 25);
    people.push_back(alice);  // alice — lvalue
    return 0;
}

Вывод:

Конструктор вызван для Alice
Конструктор копирования для Alice

Здесь мы видим копирование, а не перемещение. Если объект большой (содержит, например, динамические массивы), копирование может быть дорогим.

Вариант 3: emplace_back

int main() {
    std::vector<Person> people;
    people.emplace_back("Bob", 30);  // Передаем аргументы для конструктора
    return 0;
}

Вывод:

Конструктор вызван для Bob

Всего один вызов конструктора! Объект Person создается прямо внутри памяти вектора, без промежуточных копирований или перемещений.

Производительность: когда разница критична?

Для простых типов (int, double, указатели) разница незаметна — компилятор все оптимизирует. Но для сложных объектов разница может быть существенной:

  • Объекты с тяжелым конструктором копирования (например, std::string, контейнеры, изображения).
  • Объекты, которые нельзя копировать (например, std::unique_ptr или мьютексы).
  • Объекты с большим количеством полей — каждое копирование требует времени.

Рассмотрим пример с std::string:

std::vector<std::string> strings;

// push_back — создание + перемещение
strings.push_back("Hello, World!");  // Сначала создается const char*, потом перемещается в std::string

// emplace_back — создание на месте
strings.emplace_back("Hello, World!");  // Сразу создается std::string внутри вектора

В первом случае компилятор может применить оптимизацию (RVO/NRVO), но гарантий нет. emplace_back гарантирует in-place construction.

Подводные камни: когда emplace_back опасен?

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

1. Явные конструкторы

Если у класса есть конструктор с одним аргументом, помеченный как explicit, emplace_back может вести себя неожиданно:

class Widget {
public:
    explicit Widget(int x) { /* ... */ }
};

std::vector<Widget> widgets;
widgets.emplace_back(42);  // ОК: явный вызов конструктора
widgets.push_back(42);     // Ошибка компиляции! Неявное преобразование int -> Widget запрещено

2. Неправильный порядок аргументов

emplace_back использует perfect forwarding, поэтому аргументы передаются в конструктор как есть. Если вы перепутаете порядок, компилятор может не заметить ошибку, а программа будет вести себя странно:

struct Point {
    int x, y;
    Point(int a, int b) : x(a), y(b) {}
};

std::vector<Point> points;
points.emplace_back(10, 20);  // ОК: создает Point(10, 20)
points.emplace_back(20, 10);  // Тоже ОК, но x и y поменяны местами

3. Сложные типы с множественными конструкторами

Если у класса есть несколько конструкторов, emplace_back может вызвать не тот, который вы ожидали:

class Data {
public:
    Data(int a) { /* ... */ }
    Data(const std::string& s) { /* ... */ }
};

std::vector<Data> data;
data.emplace_back(42);     // Вызовет Data(int)
data.emplace_back("abc");  // Вызовет Data(const std::string&)

В этом нет ничего плохого, но нужно быть внимательным.

Когда что использовать?

Вот простая шпаргалка для новичков: