The emplace_back Gap: In-Place Construction and What It Requires from a Custom vector
Source: isocpp
Implementing push_back is straightforward once you understand placement new. Quasar Chunawala’s walkthrough on isocpp.org uses push_back as the main growth mechanism, which is the right pedagogical entry point. What the exercise leaves off is the question of how the element is constructed in the first place, and what happens when the caller has not yet built the element at all. That question leads to emplace_back, variadic templates, perfect forwarding, and a closer look at what std::allocator_traits::construct is actually doing.
What push_back Does to Your Constructor
The two push_back overloads in any vector implementation handle copy and move separately:
void push_back(const T& value) {
ensure_capacity();
std::allocator_traits<Alloc>::construct(alloc_, end_, value);
++end_;
}
void push_back(T&& value) {
ensure_capacity();
std::allocator_traits<Alloc>::construct(alloc_, end_, std::move(value));
++end_;
}
Both require that the element already exists as a fully constructed object before it reaches the vector. The copy overload copies it. The move overload steals its resources. In either case, the element’s constructor has already run once by the time the vector gets involved.
Consider adding an element of a type whose construction is non-trivial:
struct Record {
std::string id;
std::vector<int> data;
Record(std::string id, std::vector<int> data)
: id(std::move(id)), data(std::move(data)) {}
};
records.push_back(Record{"abc", {1, 2, 3, 4}});
The call Record{"abc", {1, 2, 3, 4}} constructs a temporary. push_back then moves that temporary into the vector’s buffer. With inlining and optimization, this often collapses to a single construction, but the interface exposes two steps and any constructor the compiler cannot see through will run twice.
The emplace_back Interface
emplace_back takes the constructor arguments directly and constructs the element in place:
records.emplace_back("abc", std::vector<int>{1, 2, 3, 4});
No temporary is created; the arguments are forwarded directly to the constructor at the target address in the vector’s buffer. The element comes into existence for the first time in its final location.
The implementation is where the design becomes concrete:
template<typename... Args>
T& emplace_back(Args&&... args) {
ensure_capacity();
std::allocator_traits<Alloc>::construct(
alloc_, end_, std::forward<Args>(args)...
);
++end_;
return *(end_ - 1);
}
Two things make this work: variadic templates and perfect forwarding. The parameter pack Args&&... args accepts any combination of argument types. std::forward<Args>(args)... preserves the value category of each argument as it travels through the template. An lvalue argument stays an lvalue; an rvalue stays an rvalue. Without std::forward, rvalue arguments would lose their rvalue-ness inside the template body and be treated as lvalues, defeating the point. This is the standard definition of perfect forwarding, and emplace_back is one of the cleanest examples of where it matters.
allocator_traits::construct as the Construction Interface
The call to std::allocator_traits<Alloc>::construct(alloc_, end_, args...) is where the construction happens. The default implementation in the standard calls:
::new(static_cast<void*>(p)) T(std::forward<Args>(args)...)
This is placement new with forwarded arguments. Routing through allocator_traits rather than calling placement new directly matters for the same reason it matters in push_back: custom allocators can override the construct member function to intercept construction. A pool allocator might track which objects are live. A diagnostic allocator might zero memory before construction. By using allocator_traits::construct as the construction interface, the vector becomes compatible with all of them without modification.
The consequence is that push_back(value) and emplace_back(value) converge on the same underlying call. push_back calls construct(alloc_, end_, value), which is new(end_) T(value). emplace_back(value) calls construct(alloc_, end_, value), which is also new(end_) T(value). For a single argument that matches a constructor parameter exactly, the two operations are equivalent. The advantage of emplace_back surfaces when multiple arguments bypass a constructor overload that would otherwise create an intermediate object, or when the argument combination enables a constructor that push_back cannot reach.
C++17’s Return Type Change
Before C++17, emplace_back returned void. P0084, adopted into C++17, changed the return type to T&, a reference to the newly constructed element.
// C++17 and later
auto& record = records.emplace_back("abc", data);
record.id = "updated"; // Direct access without calling back()
The pre-C++17 way to access the element after emplacement was records.back(), an additional call that also requires knowing the element went to the end. The T& return form eliminates that indirection.
Implementing this in a custom vector changes the return statement from void to return *(end_ - 1). After ++end_, the newly constructed element sits at end_ - 1. This is correct whether or not ensure_capacity triggered a reallocation, because end_ is always updated relative to the current buffer before the return.
When push_back and emplace_back Generate Identical Code
With optimization enabled, push_back(T{a, b}) and emplace_back(a, b) frequently produce identical assembly. The temporary in push_back(T{a, b}) is often eliminated entirely through copy elision, and construction goes directly to the target address. Compilers like GCC and Clang with -O2 can see through both call paths when the constructor and move constructor are both inlineable.
The cases where emplace_back measurably differs from push_back in generated code:
- Types whose move constructors are non-trivial and cannot be inlined across translation units
- Construction from many arguments where an intermediate object would require multiple allocations or initializations
- Types with explicit constructors, where
push_backwith an initializer list is not even a valid option
The practical guideline is to use emplace_back when constructing from arguments and push_back when inserting an already-constructed element. The semantic intent is clearer in both directions and the generated code is never worse.
The Explicit Constructor Case
There is one situation where push_back and emplace_back are not interchangeable for reasons beyond performance: explicit constructors.
struct Config {
int port;
std::string host;
explicit Config(int p, std::string h) : port(p), host(std::move(h)) {}
};
std::vector<Config> configs;
configs.push_back({8080, "localhost"}); // Error: explicit constructor
configs.push_back(Config{8080, "localhost"}); // OK: explicit construction
configs.emplace_back(8080, "localhost"); // OK: direct initialization
push_back({8080, "localhost"}) attempts implicit conversion from a brace-enclosed initializer list. An explicit constructor prevents that conversion. emplace_back(8080, "localhost") uses direct initialization, which is permitted to call explicit constructors. The cppreference documentation on emplace_back specifies this: element construction uses std::allocator_traits<A>::construct(alloc, ptr, std::forward<Args>(args)...), which calls ::new(ptr) T(std::forward<Args>(args)...), direct-initialization.
For types that mark constructors explicit to guard against accidental conversions, emplace_back is the natural insertion mechanism. A custom vector gets this behavior without additional effort because the forwarded arguments go directly to allocator_traits::construct.
The Aliasing Hazard Persists
The same aliasing problem present in push_back applies to emplace_back. If a reallocation occurs and one of the arguments refers to an element already inside the vector, the reallocation will move or destroy that element before it is used to construct the new one:
v.emplace_back(v[0]); // Hazard if reallocation occurs
For the single-argument case, the mitigation is the same as for push_back: check whether the incoming address falls within [begin_, end_) before reallocating, and if so, make a local copy first. libc++ implements this check explicitly in its push_back path; the emplace path with a single T-typed argument is handled similarly.
For variadic emplace_back with arbitrary argument types, a fully general aliasing check is harder to write because the arguments may not be convertible to element pointers at all. Standard library implementations typically protect against the obvious case and accept that the general variadic path is the caller’s responsibility to use correctly.
Pack Expansion and Reference Collapsing in Practice
Adding emplace_back to a custom vector makes variadic templates and reference collapsing concrete rather than abstract.
The declaration template<typename... Args> emplace_back(Args&&... args) uses a universal reference in a template context. When the caller passes an lvalue x, Args is deduced as T&, and Args&& collapses to T&. When the caller passes an rvalue std::move(x), Args is deduced as T, and Args&& stays T&&. This reference collapsing is the mechanism that makes perfect forwarding work, and emplace_back is one of the shortest programs that fully exercises it.
std::forward<Args>(args)... expands both the Args pack and the args pack in lockstep. Each argument is forwarded with its original value category. Without the std::forward, all arguments would be lvalues inside the template body, and move-only arguments could not be passed through.
Implementing this correctly in a custom vector before encountering it in production code builds a mental model that makes reading any forwarding-heavy library considerably easier. The syntax is unfamiliar the first time; understanding it through implementation makes it mechanical.
What the Exercise Adds
Adding emplace_back to the vector exercise Chunawala describes requires three mechanisms that push_back does not: variadic templates for the parameter pack, perfect forwarding to preserve value categories, and allocator_traits::construct as the authoritative construction interface. Each of these exposes a different layer of how generic C++ works.
The variadic template exposes pack expansion syntax in a context where the behavior is immediately observable. Perfect forwarding exposes reference collapsing in a context where getting it wrong produces a measurable regression. allocator_traits::construct exposes the separation between allocation policy and construction policy that makes the allocator model coherent. Together they cover a compact but meaningful slice of the template machinery that underlies most of the standard library, and the vector implementation provides a concrete frame for each one.