Can emplace_back replace push_back?

The emplace_back method of a sequential STL container can mostly replace the push_back method.

Tutorial | Nov 10, 2019 | nextptr 

Introduction

The perfect forwarding and variadic function templates together, brought a seamless emplacement (or placement insertion) to the STL containers. With emplacement, it is possible to construct an object directly inside a container from the user arguments without creating a temporary object, which can yield better performance. Here is a simple example of in-place construction:

template<typename T>
class Box {
public:
 //Variadic function template
 template<typename ...Args>
 void set(Args&& ...args) {
  /*In place construction with placement-new.
    Arguments are perfect-forwarded.*/
  new (place) T(std::forward<Args>(args)...);
 }

 const T& get() const { return *((T*)place); }
 //....
private:
 alignas(T) uint8_t place[sizeof(T)];
};

A Box can store an arbitrary object, which can be directly built inside the Box by calling the variadic function template set. The placement-new is used to invoke the constructor of the stored object at a specified memory location (place). The alignment requirement of place is specified as of T using the alignas specifier.

Here is an example of using Box to store an arbitrary type Point:

struct Point {
 Point() = default;
 Point(int x_, int y_)
    :x(x_),y(y_){}
 int x, y;
};

int main() {

 Box<Point> p; //'p' is a Box that can contain a Point
 p.set(10, 20); //Constructs Point in-place

 std::cout << p.get().x << ","
          << p.get().y << "\n"; //10,20 
 return 0;
}

In general, the emplacement can nearly always be used instead of the regular insertion to insert an element in an STL container. The emplacement would not always result in better performance, but it should never have worse performance. It is presented here as an example that the emplace_back method of sequential containers (vector, list, and deque) can be used in place of the push_back method in almost all situations. Also, most of the argument given here applies to emplace and insert methods of associative containers (e.g., map) too.

push_back vs. emplace_back 🤼‍♀️

The std::vector (also std::list and std::deque) has the following methods to append an element in the end:

// Regular insert
void push_back(const T& value);
void push_back(T&& value);
// emplace insert
template< class... Args >
reference emplace_back(Args&&... args); //returns reference since C++17

There is no difference between emplace_back or push_back when the argument is of the same type as of the container's element T. In this case either a copy of the passed argument is created, or it is moved:

std::vector<std::string> v;

std::string hello = "Hello";
v.push_back(hello); //copy
v.push_back(std::move(hello)); //move

//Same with emplace_back 
std::string world = "World";
v.emplace_back(world); //copy
v.emplace_back(std::move(world)); //move

Nevertheless, the story is different when the argument's type is different from the type of container's element. When using push_back, either a temporary object is implicitly created or has to be constructed explicitly:

v.push_back("Hello"); //temporary moved
// is same as
v.push_back(std::string("Hello")); //temporary moved

But with emplace_back, there is no temporary object created because the element object is built in-place from the passed arguments:

v.emplace_back("World"); //in-place construction

This shows that the emplace_back can do everything that the push_back can do, and it can do more.

However, there are a handful of situations, outlined below, where the push_back, or rather regular insertion, could be a more fitting choice.

Possibility of Exception and Resource Leak ☠️

Consider an STL container of std::unique_ptr, e.g., std::vector<std::unique_ptr<File>>. A new element is emplaced into this vector as follows:

struct File { /*...*/ };
std::vector<std::unique_ptr<File>> files;
//emplace insert
files.emplace_back(new File(/*..*/)); 

Suppose the vector needs to allocate more memory to store the new element. But an out-of-memory exception is raised while allocating the memory before even the new std::unique_ptr is constructed in-place. That exception is thrown to the emplace caller, and the raw File pointer is lost, causing a memory leak. This situation cannot arise with the push_back because a temporary std::unique_ptr would be constructed before entering the container. Therefore, when it comes to containers of resource managing objects (e.g., std::unique_ptr or std::shared_ptr), we should let go of the performance benefits of emplacement and use regular insertion by using either emplace_back or push_back:

files.emplace_back(std::unique_ptr<File>(new File(/*..*/))); //temp moved
// is same as
files.push_back(std::unique_ptr<File>(new File(/*..*/))); //temp moved

Even better, we should always use std::make_unique to create an std::unique_ptr whenever possible:

 files.emplace_back(std::make_unique<File>(/*..*/)); //temp moved
Aggregates and Brace-Initialization

An aggregate (usually, a plain old struct) can be brace-initialized and inserted using push_back:

struct Point { int x,y; };

std::vector<Point> points;
//insert with brace-initialization
points.push_back({10,20});

Unfortunately, as of now, there is no-way to brace-initialize an aggregate using emplace. Unlike the previous one, this point could be simply a matter of a little inconvenience, but only if you know that it is merely a superficial matter. Until then, it could be very confounding.

Quite reasonably, using emplace_back to brace-initialize and insert fails to compile because the brace-list cannot resolve to the struct:

points.emplace_back({10,20}); //ERROR

What is more vexing is that the standard emplace also does not work because the struct does not have a required constructor defined:

points.emplace_back(10,20); //ERROR

We must define a constructor for the struct, or turn it into a non-aggregate, to make it emplace-able:

struct Point { 
 Point(int x_,int y_):x(x_),y(y_){}
 int x,y; 
};

//good now
points.emplace_back(10,20);

Finally

What is presented here applies to all standard containers' emplace and insert methods. However, the benefits of emplacement in associative containers (e.g., set) also have to be weighed against the fact that these containers might discard the duplicate values.

For instance, a duplicate value is rejected in the following code, but an object still has to be constructed to compare against the value in the container:

std::set<std::string> s;
s.emplace("Hello");

//An std::string is constructed/destroyed
s.emplace("Hello");

One has to be careful because the lauded advantages of emplacement might obscure the wasteful cost of construction and destruction of duplicate values.

Further Reading 📚

std::vector emplace_back method: cppreference

Effective Modern C++: Scott Meyers