Введение: почему это важно?
Вы когда-нибудь задумывались, почему в 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&)
В этом нет ничего плохого, но нужно быть внимательным.
Когда что использовать?
Вот простая шпаргалка для новичков:
-
Блоги
Рекомендаций по книгам
Читайте также
Внедрение зависимостей (Dependency Injection) в Rust для начинающих
JavaScript и джентльмены: зачем вам паттерны проектирования (и как они упростят жизнь)
Парсинг на TypeScript: как скрапить данные быстрее и надежнее
SOLID принципы в C++: теория и практика от А до Я
Асинхронное программирование в TypeScript: полное руководство