C++ emplace vs push: Performance and Move Semantics
Key takeaways
emplace_back is often assumed to always be faster, but with a cheap-to-move temporary the gap can vanish. The post shows where emplace constructs in place, how to benchmark the difference honestly, and the real traps: explicit constructors, surprising overloads, and forwarding in loops.
Why emplace Exists
Problem: Temporary Object Overhead
Problem: push_back creates a temporary object then moves/copies it into the container.
std::vector<std::string> vec;
vec.push_back(std::string("hello")); // 1. Construct temporary
// 2. Move into vector
Solution: emplace_back constructs the object in-place inside the container, avoiding temporary.
std::vector<std::string> vec;
vec.emplace_back("hello"); // Construct directly in vector
flowchart TD
subgraph push[push_back]
p1["1. Construct temp"]
p2["2. Move to container"]
p1 --> p2
end
subgraph emplace[emplace_back]
e1["1. Construct in-place"]
end
Before C++11 the difference was large, because the second step of push_back was a copy: push_back(std::string("hello")) allocated the string twice. Move semantics turned that second step into a move, which for std::string is copying three pointers-worth of data (or, for short strings stored inline, a few bytes). So the question today is not “copy versus no copy” but “one move versus none”, and that is a much smaller gap. emplace_back still matters where a move is expensive or impossible, and it is simply more natural when you have constructor arguments rather than an object.
Basic Comparison
Syntax
#include <vector>
#include <string>
std::vector<std::string> vec;
// push_back: pass object
vec.push_back(std::string("hello"));
vec.push_back("world"); // Implicit conversion
// emplace_back: pass constructor arguments
vec.emplace_back("hello");
vec.emplace_back(5, 'a'); // std::string(5, 'a') = "aaaaa"
push_back("world") compiles because push_back takes a const std::string& or std::string&&, and the compiler implicitly converts the literal into a temporary std::string first. emplace_back is a variadic template that forwards its arguments to any constructor of the element type, which is what makes emplace_back(5, 'a') possible and also what makes it more permissive, as the pitfalls section shows.
Key Differences
| Aspect | push_back | emplace_back |
|---|---|---|
| Arguments | Takes object | Takes constructor args |
| Construction | External then move | In-place |
| Temporaries | May create temporary | Avoids temporary |
| Forwarding | No | Perfect forwarding |
In-Place Construction
Example: Complex Object
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {
std::cout << "Point(" << x << ", " << y << ")\n";
}
Point(const Point& p) : x(p.x), y(p.y) {
std::cout << "Copy Point\n";
}
Point(Point&& p) : x(p.x), y(p.y) {
std::cout << "Move Point\n";
}
};
int main() {
std::vector<Point> vec;
vec.reserve(2); // without this, the second insert reallocates and prints "Copy Point"
std::cout << "push_back:\n";
vec.push_back(Point(1, 2)); // 1. Construct temp
// 2. Move to vector
std::cout << "\nemplace_back:\n";
vec.emplace_back(3, 4); // Construct directly in vector
}
Output:
push_back:
Point(1, 2)
Move Point
emplace_back:
Point(3, 4)
Key: emplace_back avoids the move operation.
The reserve(2) line matters for this output, and leaving it out teaches a separate lesson. Without it, the vector’s capacity is 1 after the first insert, so the second insert reallocates and must transfer the existing element. Because this Point move constructor is not declared noexcept, std::vector copies instead of moving during reallocation, to preserve its strong exception guarantee, and you see an extra Copy Point line. In real code that reallocation cost usually outweighs the push/emplace difference by far. Marking move constructors noexcept (or leaving them compiler-generated, which are noexcept when the members’ moves are) and calling reserve when the size is known are the bigger wins.
Perfect Forwarding
struct Data {
std::string name;
int value;
Data(std::string n, int v) : name(std::move(n)), value(v) {
std::cout << "Data(" << name << ", " << value << ")\n";
}
};
int main() {
std::vector<Data> vec;
std::string s = "test";
// push_back: construct temporary
vec.push_back(Data(s, 42));
// emplace_back: perfect forwarding
vec.emplace_back(s, 42); // Forwards s by lvalue reference
vec.emplace_back(std::move(s), 100); // Forwards by rvalue reference
}
Key: emplace_back uses perfect forwarding to pass arguments directly to constructor.
Follow the string through each call. push_back(Data(s, 42)) copies s into the constructor parameter n, moves n into name, and then moves the whole temporary Data into the vector. emplace_back(s, 42) forwards s as an lvalue reference, so the parameter n is still a copy of s and name is moved from it, but the final move of Data disappears. emplace_back(std::move(s), 100) forwards an rvalue, so n is move-constructed and no characters are copied at all; after this line s is in a valid but unspecified state (in practice empty) and should not be read. The savings come from the argument’s value category, which emplace_back preserves; the Data constructor taking std::string by value and moving it is what lets both lvalues and rvalues be handled efficiently with one constructor.
Performance Benchmarks
Benchmark: String Construction
#include <benchmark/benchmark.h>
#include <vector>
#include <string>
static void BM_PushBack(benchmark::State& state) {
for (auto _ : state) {
std::vector<std::string> vec;
for (int i = 0; i < 10000; ++i) {
vec.push_back(std::string("test"));
}
benchmark::DoNotOptimize(vec);
}
}
static void BM_EmplaceBack(benchmark::State& state) {
for (auto _ : state) {
std::vector<std::string> vec;
for (int i = 0; i < 10000; ++i) {
vec.emplace_back("test");
}
benchmark::DoNotOptimize(vec);
}
}
BENCHMARK(BM_PushBack);
BENCHMARK(BM_EmplaceBack);
What to expect: with short strings like "test", the two versions are usually within noise of each other, and the numbers depend heavily on the compiler, the standard library and the machine, so run the benchmark yourself rather than trusting figures from articles (including this one). The reason is visible in the code: "test" fits in the small-string buffer, so the temporary’s move copies a few bytes and no heap allocation happens in either version. Both loops are dominated by the vector’s reallocations, since neither calls reserve; for 10,000 elements, the vector reallocates and moves every existing element a dozen or so times. Adding vec.reserve(10000) to both is the first change to make before comparing, otherwise you are mostly measuring growth.
Key: For simple types, difference is small due to move optimization.
Benchmark: Complex Object
struct HeavyObject {
std::vector<int> data;
std::string name;
HeavyObject(int size, std::string n)
: data(size), name(std::move(n)) {}
};
static void BM_PushBackHeavy(benchmark::State& state) {
for (auto _ : state) {
std::vector<HeavyObject> vec;
for (int i = 0; i < 1000; ++i) {
vec.push_back(HeavyObject(100, "test"));
}
benchmark::DoNotOptimize(vec);
}
}
static void BM_EmplaceBackHeavy(benchmark::State& state) {
for (auto _ : state) {
std::vector<HeavyObject> vec;
for (int i = 0; i < 1000; ++i) {
vec.emplace_back(100, "test");
}
benchmark::DoNotOptimize(vec);
}
}
What to expect: emplace_back tends to show a small, measurable advantage here, because moving a HeavyObject means moving a std::vector and a std::string (several pointer copies and resets) that emplace_back skips. The allocation inside data(size) happens in both versions and dominates each iteration, so the relative difference stays modest. The case where emplace_back wins decisively is a type whose move is expensive or unavailable, such as a class holding a large std::array by value or a type with a deleted move constructor, where push_back of a temporary would copy.
Key: For complex objects, emplace_back can show a measurable improvement; measure with your own types.
Move Semantics
push_back with Move
std::vector<std::string> vec;
std::string s = "hello";
// Copy
vec.push_back(s); // s is copied
// Move
vec.push_back(std::move(s)); // s is moved (now empty)
emplace_back with Move
std::vector<std::string> vec;
std::string s = "hello";
// emplace_back forwards arguments
vec.emplace_back(s); // Copy (lvalue)
vec.emplace_back(std::move(s)); // Move (rvalue)
Key: Both support move semantics; emplace_back uses perfect forwarding.
When Move is Cheap
struct Trivial {
int x, y;
};
std::vector<Trivial> vec;
// push_back and emplace_back are equivalent
vec.push_back({1, 2});
vec.emplace_back(1, 2); // C++20 only: parenthesized aggregate initialization
Key: For trivial types, compiler optimizes both to same code.
One version detail: Trivial is an aggregate with no constructor, so before C++20 emplace_back(1, 2) does not compile (no matching function for call to 'Trivial::Trivial(int, int)'), because the allocator constructs elements with parentheses. C++20 allows aggregates to be initialized with parentheses and makes it work. push_back({1, 2}) works in every standard, which is one of the rare cases where push_back is the more flexible call.
Common Pitfalls
Pitfall 1: Explicit Constructors
Symptom: emplace_back can bypass explicit constructors, causing unexpected conversions.
struct Data {
explicit Data(int x) : value(x) {}
int value;
};
std::vector<Data> vec;
// vec.push_back(42); // Error: explicit constructor
vec.emplace_back(42); // OK (but may be unintended)
Solution: Use push_back to enforce explicit constructor checks.
This is the most important difference in practice, and it matters more than performance. explicit exists to stop accidental conversions, and emplace_back calls constructors directly, so it ignores explicit by design. The consequences are sometimes surprising:
std::vector<std::vector<int>> vv;
vv.push_back(10); // error: no implicit conversion from int to vector<int>
vv.emplace_back(10); // compiles: appends a vector of 10 zeros
std::vector<std::unique_ptr<Widget>> widgets;
widgets.emplace_back(new Widget); // leaks if the vector's reallocation throws
widgets.push_back(std::make_unique<Widget>()); // safe: ownership taken before the call
The first pair shows emplace_back silently selecting the vector(size_type) constructor, which a typo or a wrong variable can trigger without any warning. The second is the real exception-safety trap: with emplace_back(new Widget), the raw pointer is not owned by anything until the unique_ptr is constructed inside the vector, so if allocating the vector’s new buffer throws std::bad_alloc, the Widget leaks. push_back(std::make_unique<Widget>()) (or emplace_back(std::make_unique<Widget>())) creates the owner first. Clang-Tidy’s modernize-use-emplace check is deliberately conservative about these cases for the same reasons.
Pitfall 2: Initializer Lists
Symptom: emplace_back cannot deduce initializer lists.
std::vector<std::vector<int>> vec;
// ✅ push_back with initializer list
vec.push_back({1, 2, 3});
// ❌ emplace_back cannot deduce
// vec.emplace_back({1, 2, 3}); // Error
// ✅ Explicit type
vec.emplace_back(std::vector<int>{1, 2, 3});
Solution: Use push_back for initializer lists.
The reason is template argument deduction: a braced list like {1, 2, 3} has no type, so it cannot deduce the Args... of emplace_back. push_back has a concrete parameter type (std::vector<int>&&), so the braced list simply initializes it.
Pitfall 3: Exception Safety
Symptom: A common belief is that emplace_back is less exception-safe because it constructs in place. For std::vector that is not the case.
struct Throwing {
Throwing(int x) {
if (x < 0) throw std::runtime_error("negative");
}
};
std::vector<Throwing> vec;
try {
vec.emplace_back(-1); // Throws during construction
} catch (...) {
// vec is unchanged: same size, same elements
}
Solution: Rely on the container’s guarantee, and make move constructors noexcept.
Both push_back and emplace_back on a std::vector provide the strong guarantee: if the element’s constructor throws, or allocating new storage fails, the vector is left exactly as it was. The implementation constructs the new element (in new storage, if reallocating) before touching the old elements, and only commits when everything succeeded. The guarantee has one condition: during reallocation, existing elements are moved only if their move constructor is noexcept (or the type is not copyable); otherwise they are copied so that a failure can be rolled back. If the type is move-only with a throwing move constructor, the guarantee weakens to “valid but unspecified”. Inserting in the middle (emplace(pos, ...)) offers only the basic guarantee for the same reason.
Production Patterns
Pattern 1: Reserve + emplace_back
std::vector<std::string> vec;
vec.reserve(1000); // Avoid reallocations
for (int i = 0; i < 1000; ++i) {
vec.emplace_back("item_" + std::to_string(i));
}
Key: Combine reserve with emplace_back for best performance.
Pattern 2: Factory Pattern
template<typename T, typename... Args>
std::vector<T> make_vector(std::size_t count, Args&&... args) {
std::vector<T> vec;
vec.reserve(count);
for (std::size_t i = 0; i < count; ++i) {
vec.emplace_back(args...); // not std::forward: args are reused on every iteration
}
return vec;
}
auto vec = make_vector<std::string>(100, 10, 'x'); // 100 strings of "xxxxxxxxxx"
Key: Perfect forwarding with emplace_back for generic factories.
The comment in the loop marks a real bug that is easy to write: std::forward<Args>(args)... inside a loop would move from an rvalue argument on the first iteration and pass a moved-from object on every later one. Calling make_vector<std::string>(3, std::string("abc")) with forwarding produces "abc", "", "" with typical implementations. Forward an argument only at its last use; when the same arguments are used repeatedly, pass them as lvalues. (For this particular case, std::vector<std::string>(100, std::string(10, 'x')) does the same without a helper.)
Pattern 3: Conditional Insertion
std::vector<Data> vec;
for (const auto& item : input) {
if (validate(item)) {
vec.emplace_back(process(item));
}
}
Key: Use emplace_back when constructing from processed data.
Pattern 4: Map Insertion
std::map<int, std::string> map;
// emplace: construct in-place
map.emplace(1, "one");
map.emplace(std::piecewise_construct,
std::forward_as_tuple(2),
std::forward_as_tuple(5, 'x')); // "xxxxx"
// insert: pass pair
map.insert({3, "three"});
Key: emplace for maps uses piecewise_construct for complex keys/values.
Associative containers have a twist: map.emplace(key, value) may construct the node, including the value, before checking whether the key already exists, and then throw the node away if it does. If constructing the value is expensive, or the value is a move-only object you do not want consumed, that is wasteful or wrong: map.emplace(k, std::move(ptr)) can leave ptr moved-from even though nothing was inserted. C++17’s try_emplace(key, args...) fixes this by looking up the key first and constructing the value only on insertion, and it also avoids the piecewise_construct boilerplate for the value. insert_or_assign is the counterpart for “insert or overwrite”. For maps, try_emplace is usually the call to reach for.
Complete Example
#include <iostream>
#include <vector>
#include <string>
#include <chrono>
struct Task {
std::string name;
int priority;
std::vector<int> data;
Task(std::string n, int p, std::size_t size)
: name(std::move(n)), priority(p), data(size) {
std::cout << "Task(" << name << ", " << priority << ", " << size << ")\n";
}
Task(const Task&) {
std::cout << "Copy Task\n";
}
Task(Task&&) noexcept {
std::cout << "Move Task\n";
}
};
int main() {
std::vector<Task> tasks;
tasks.reserve(3);
std::cout << "=== push_back ===\n";
tasks.push_back(Task("task1", 1, 100));
std::cout << "\n=== emplace_back ===\n";
tasks.emplace_back("task2", 2, 200);
std::cout << "\n=== push_back with move ===\n";
Task t3("task3", 3, 300);
tasks.push_back(std::move(t3));
std::cout << "\n=== Performance Test ===\n";
auto start = std::chrono::high_resolution_clock::now();
std::vector<Task> vec1;
vec1.reserve(10000);
for (int i = 0; i < 10000; ++i) {
vec1.push_back(Task("task", i, 10));
}
auto end = std::chrono::high_resolution_clock::now();
auto push_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
start = std::chrono::high_resolution_clock::now();
std::vector<Task> vec2;
vec2.reserve(10000);
for (int i = 0; i < 10000; ++i) {
vec2.emplace_back("task", i, 10);
}
end = std::chrono::high_resolution_clock::now();
auto emplace_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
std::cout << "push_back: " << push_time << " μs\n";
std::cout << "emplace_back: " << emplace_time << " μs\n";
std::cout << "Improvement: " << (100.0 * (push_time - emplace_time) / push_time) << "%\n";
}
Output:
=== push_back ===
Task(task1, 1, 100)
Move Task
=== emplace_back ===
Task(task2, 2, 200)
=== push_back with move ===
Task(task3, 3, 300)
Move Task
=== Performance Test ===
Task(task, 0, 10)
Move Task
... (timings vary by machine)
Key: emplace_back avoids move construction, improving performance for complex objects.
The first three sections of output are the useful part: they show exactly one Move Task for the push_back of a temporary, none for emplace_back, and one for the explicit std::move(t3). The timing section, on the other hand, is not a meaningful benchmark as written. Every construction prints to std::cout, and 20,000 lines of console output take far longer than the moves being compared, so the result mostly measures terminal speed and varies from run to run. Use a benchmarking library such as Google Benchmark with printing removed, as in section 3, for real numbers. Note also that the tracing copy and move constructors here deliberately do not copy the members, so moved or copied Tasks have empty names and no data; that is fine for demonstrating which constructor runs, but a real class should use the defaulted versions.
When to Use Each
Use push_back
- Already have object:
vec.push_back(existing_obj); - Initializer lists:
vec.push_back({1, 2, 3}); - Clarity over micro-optimization: More explicit intent
- Explicit constructors: Enforce type safety
Use emplace_back
- Constructing in-place:
vec.emplace_back(arg1, arg2); - Complex objects: Avoid temporary + move overhead
- Perfect forwarding: Generic code with variadic templates
- Performance-critical: Measured improvement
When emplace_back is the wrong choice
A simple rule covers most code: use push_back when you already have an object of the element type, and emplace_back when you are passing the arguments of a constructor. The cases where emplace_back hurts are the ones where it calls a constructor you did not mean to call.
Because it forwards arguments directly to a constructor, emplace_back can use explicit constructors that push_back would refuse. With std::vector<std::vector<int>> v, v.emplace_back(10) appends a vector of ten zeros, while v.push_back({10}) appends a vector containing the single value 10. With a vector of std::unique_ptr<T>, v.emplace_back(new T) can leak: if the vector has to reallocate and the allocation throws, the raw pointer was never owned by anything. v.push_back(std::make_unique<T>()) has no such gap. When a reviewer cannot tell which constructor an emplace_back call selects, push_back with an explicitly constructed object is the clearer code.
FAQ
Q1: emplace always faster?
A: No. For trivial types or cheap moves, difference is negligible. Benchmark for your use case.
Q2: Can I use emplace with initializer lists?
A: No. emplace_back({1, 2, 3}) won’t compile. Use push_back({1, 2, 3}) or explicit constructor.
Q3: What about exception safety?
A: For std::vector, emplace_back and push_back both leave the vector unchanged if construction or reallocation throws, as long as the element type is copyable or has a noexcept move constructor. The trap to avoid is passing a raw new expression to emplace_back on a container of smart pointers.
Q4: Other containers?
A: emplace family exists for all containers:
vector:emplace_backdeque:emplace_back,emplace_frontlist:emplace_back,emplace_frontmap:emplace,try_emplaceset:emplace,emplace_hint
Q5: Compiler support?
A: C++11 and later. All major compilers (GCC, Clang, MSVC) fully support.