· 5 min read ·

From push_back to emplace_back: How In-Place Construction Works Inside vector

Source: isocpp

When you implement vector<T> from scratch, push_back is the natural first step. Take a value, check capacity, call placement new, increment the size counter. Quasar Chunawala’s walkthrough on isocpp.org gets you there early. Then you encounter emplace_back, and its template signature looks meaningfully different. The difference is real, but it is narrower than the commonly repeated advice suggests.

What push_back Accepted Before C++11

In C++03, push_back took a const T&. Every element was copied into the vector. If you wrote v.push_back(MyObject(a, b, c)), two things happened: a temporary MyObject was constructed on the stack, then it was copy-constructed into the vector’s buffer. Copy elision applied only to return values, not to function arguments, so the copy was unavoidable.

C++11 added an rvalue reference overload:

void push_back(const T& value);  // copy into vector
void push_back(T&& value);       // move into vector

With the second overload, v.push_back(MyObject(a, b, c)) moves the temporary instead of copying it. For types where a move is a pointer steal, this is substantially cheaper. But the implementation still involves two steps: construct a T, then construct it again (via move) into the vector’s buffer. The temporary exists, even briefly.

What emplace_back Does

emplace_back takes constructor arguments rather than a constructed value:

template<typename... Args>
T& emplace_back(Args&&... args);

The implementation uses perfect forwarding directly into placement new:

template<typename... Args>
T& emplace_back(Args&&... args) {
    if (_end == _end_cap)
        grow();
    ::new(static_cast<void*>(_end)) T(std::forward<Args>(args)...);
    return *_end++;
}

There is no temporary T. T(std::forward<Args>(args)...) constructs the object at the address where it will live in the buffer. If T has a constructor taking (int, std::string), the arguments go straight to that constructor, in-place. cppreference’s documentation for emplace_back notes that the return type changed from void to T& in C++17, letting you access the newly constructed element without a separate back() call:

auto& w = v.emplace_back(a, b, c);
w.configure();

The contrast with push_back matters most for types with non-trivial constructors and for arguments that are not already of type T:

std::vector<std::string> vs;
vs.push_back("hello");    // constructs a temporary std::string, then moves it in
vs.emplace_back("hello"); // constructs std::string directly from const char*

For a string long enough to require heap allocation, the eliminated temporary means one fewer allocation and one fewer copy. For short strings under the small string optimization, the difference is negligible.

When the Difference Evaporates

For trivially constructible types and for insertions of pre-existing objects, push_back and emplace_back are functionally identical and compilers emit the same code. Writing v.emplace_back(42) where the vector holds int buys nothing.

More importantly, when inserting an existing object, push_back communicates intent more clearly:

Widget w = make_widget();
v.push_back(w);             // copy, clearly
v.push_back(std::move(w));  // move, clearly
v.emplace_back(w);          // also copies, but looks like something different is happening

emplace_back(w) looks like it might invoke in-place magic. It does not. It calls T’s copy constructor, exactly as push_back(w) would. The advice to always prefer emplace_back treats a performance argument as a universal rule and ends up obscuring code that would be clearer without it.

The C++20 Aggregate Change

Before C++20, emplace_back did not work with aggregates that had no user-declared constructor:

struct Point { float x, y; };

std::vector<Point> pts;
pts.emplace_back(1.0f, 2.0f);  // error before C++20: no matching constructor
pts.push_back({1.0f, 2.0f});   // works: brace initialization applies

The issue was that T(args...) in a placement new expression did not invoke aggregate initialization before C++20. P0960R3, adopted for C++20, extended parenthesized initialization to cover aggregates. The emplace_back call above compiles under C++20 without adding a constructor to Point. On older standards, push_back with brace initialization is the correct approach for aggregates.

Implementing Both in a Custom Vector

Once you have emplace_back, push_back becomes a thin wrapper:

void push_back(const T& val) { emplace_back(val); }
void push_back(T&& val)      { emplace_back(std::move(val)); }

The grow() function that emplace_back calls is where the most important detail lives. It must use std::move_if_noexcept when transferring existing elements to the new buffer:

void grow() {
    size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
    T* new_buf = static_cast<T*>(::operator new(new_cap * sizeof(T)));
    size_t n = size();
    for (size_t i = 0; i < n; ++i) {
        ::new(static_cast<void*>(new_buf + i)) T(std::move_if_noexcept(_begin[i]));
        _begin[i].~T();
    }
    ::operator delete(_begin);
    _begin = new_buf;
    _end = new_buf + n;
    _end_cap = new_buf + new_cap;
}

If the element type’s move constructor is noexcept, std::move_if_noexcept produces an rvalue and the element is moved. If not, it produces an lvalue and the element is copied. This preserves the strong exception guarantee: if anything throws during reallocation, the old buffer is still intact and the vector can be left unchanged.

Skipping std::move_if_noexcept and always moving is a common mistake in homegrown implementations. Types with throwing move constructors will then corrupt the vector mid-reallocation if an exception fires, with no recovery path. The standard implementation never makes this mistake, and reading libstdc++‘s source for _M_realloc_insert shows the exact sequencing that keeps the guarantee intact.

One Genuine Gotcha

emplace_back accepts anything that T’s constructors accept, which means it can silently invoke implicit conversions you did not intend:

std::vector<bool> flags;
flags.push_back(nullptr);   // error: no matching call
flags.emplace_back(nullptr); // compiles: nullptr converts to bool

The push_back version fails at compile time because there is no implicit conversion from nullptr_t to bool via the call chain. The emplace_back version forwards nullptr directly to bool’s constructor and silently produces false. This is a narrow case, but it illustrates why emplace_back’s flexibility is a double-edged property when type safety is the priority.

The Practical Rule

emplace_back is worth preferring when you are constructing an element from non-T arguments and the construction cost is meaningful: building a string from a literal, constructing a compound object from its constructor arguments without an intermediate object, or any case where avoiding a temporary matters. For those cases, the in-place construction is the right abstraction.

push_back is clearer for copying or moving pre-existing objects, for trivially constructible types, and anywhere the cost difference is zero. The common advice to always use emplace_back is a useful default, but it becomes precise only when you understand what the implementation does. When the implementation is:

::new(static_cast<void*>(_end)) T(std::forward<Args>(args)...);

the question is whether forwarding constructor arguments directly differs meaningfully from constructing a T first and then moving it. For some types and call sites, it does. For many, it does not. Knowing which is which comes from understanding what T’s constructor does and what the forwarding actually avoids.

Was this interesting?