[Modern C++ Series] push_back or emplace_back ??To insert or To emplace ?[Part2]

JustIdeas
7 min readJan 23, 2023

--

Are you tired of just hearing about the theory behind insert() and emplace() ? Want to see the difference in action? In my last post, I delved into the nitty-gritty of when to use push_back and emplace_back, but I received a lot of requests for concrete examples to really drive the point home.

So, in this post, I’m excited to dive deeper and show you real-life scenarios where push_back (insertion functions in general) truly shine over emplace_back (emplacement functions in general). Get ready to see the power of these functions in action! Because seeing is believing.

We are going to address the code examples in two parts-

  1. Code to show how emplace_back and push_back work internally.
  2. Code to show scenarios where emplace functions are not optimal.

Before we dive into the fun part (coding, of course!) I strongly suggest taking a quick look at my previous post to get a solid understanding of the concepts. Trust me, it’ll save you from drawing the wrong conclusions while looking at the code. Plus, you’ll truly grasp the underlying behavior. Here’s the link to the previous post -https://medium.com/@its.me.siddh/modern-c-series-vector-push-back-or-emplace-back-e3a482ab4dcd

How push_back and emplace_back work internally ?

To truly get a handle on how emplace_back and push_back work, it’s crucial to see the difference in action by using a custom class. By creating our own class and adding print statements to the constructors and destructors, we can gain a hands-on understanding of the mechanics at play. Here we go-

user-defined class “MyString”

#pragma once
#include <iostream>

class MyString {
private:
char* data_;
int length_;

public:
// Default constructor
MyString() : data_(nullptr), length_(0)
{
std::cout << "\nDefault constructor called" << std::endl;
}

// Constructor with initial string
MyString(const char* str)
{
std::cout << "\nparameterised constructor called" << std::endl;
length_ = strlen(str);
data_ = new char[length_ + 1];
strcpy(data_, str);
}

// Copy constructor
MyString(const MyString& other) : data_(other.data_), length_(other.length_){

std::cout << "\nCopy constructor called" << std::endl;
}

// Move constructor
MyString(MyString&& other) : data_(other.data_), length_(other.length_)
{
other.data_ = nullptr;
other.length_ = 0;
std::cout << "\nMove constructor called" << std::endl;
}

// Assignment operator
MyString& operator=(const MyString& other)
{
if (this != &other)
{
delete[] data_;
data_ = other.data_;
length_ = other.length_;
}
std::cout << " \n assignment called" << std::endl;
return *this;
}

// Move assignment operator
MyString& operator=(MyString&& other)
{
if (this != &other)
{
delete[] data_;
data_ = other.data_;
length_ = other.length_;

other.data_ = nullptr;
other.length_ = 0;
}
std::cout << "\nMove assignment called" << std::endl;
return *this;
}

// Destructor
~MyString()
{
std::cout << "\nDestructor called\n";

}


};

main funtion-

#include<iostream>
#include<vector>
#include"MyString.h"
using namespace std;

void TestEmplace_back()
{
std::cout << __FUNCSIG__<<endl;

std::vector<MyString> myVector;
myVector.emplace_back("Hello");
// Constructs a new string object "Hello" in-place and stores it in the vector

cout << "\n########################\n";
MyString str{ "world" }; // parameterised constructor call
myVector.emplace_back(str); // The object already exist and we are just emplacing it into vector
return;

}
void TestPush_back()
{
std::cout << __FUNCSIG__ << endl;

std::vector<MyString> myVector;
myVector.push_back("Hello");
// Constructs a new string object "Hello" and stores it in the vector
cout << "\n########################\n";
MyString str("world");
myVector.push_back(str);
return;

}



int main()
{
TestPush_back();
cout << "\n------------------------------------------\n";
TestEmplace_back();

return 0;
}

Output:

Output highlighted in Blue is where emplace_back behaving similar to push_back

Lets discuss the output

Area marked with Green color — When we use push_back method to insert an object into a vector, it creates the object first then moves it into the vector. After that, it destroys the object created. This process can be observed in the output window as two constructor calls followed by a destructor call.

Why is so ?

In the call —

myVector.push_back(“Hello”);
compilers see a mismatch between the type of the argument (const char[5]) and the type of the parameter taken by push_back (a reference to a MyString). They address the mismatch by generating code to create a temporary std::MyString object from the string literal, and then pass that temporary object to push_back. In other words, they treat the call as if it had been written like this:

myVector.push_back(MyString(“Hello”)); // create temp. std::string
// and pass it to push_bac

Area marked with Yellow color — Here, we are using emplace_back() to store the object in vector. We can see only one parameterized constructor call was made. That is because emplace_back() uses whatever arguments are passed to it to construct a MyString directly inside the std::vector. No temporaries are involved. As, emplace_back() internally uses perfect forwarding(topic for another day).

Text highlighted with Blue color —Let’s talk about this interesting scenario where emplace_back and push_back seem to do the same job. If we take a closer look at the number of constructor and destructor calls in both cases, it may come as a surprise that they’re the same! This is because when we already have the object at hand, emplace_back doesn’t offer any additional benefits over push_back. It’s like having a fancy tool, but if the job doesn’t require it, a simple hammer will do the same job. This is why it’s important to understand when to use emplace_back and when to use push_back, so you can make the most efficient use of these functions.

Scenarios, where emplace functions, are not the best choice-

In my previous post, I highlighted three scenarios in which the emplace functions have been known to shine, outperforming their insertion counterparts.

But what happens when these conditions are not met? Let’s dive into some code examples and explore the results together. It’s like trying to make a fancy dish, even if you have all the best ingredients if you miss a step or don’t use the right amount it may not turn out as good as you expect.

#include <set>
#include <iostream>
#include <chrono>
#include <random>

int main() {
std::set<int> mySet;
std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(0,7500);

auto start = std::chrono::high_resolution_clock::now();
for(int i = 0; i < 10000; i++)
{
mySet.emplace(dist(rng)); //using emplace funtion
}
auto end = std::chrono::high_resolution_clock::now();

std::chrono::duration<double> elapsed = end-start;
std::cout << "Time taken to emplace : " << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count() << "s\n";

mySet.clear();

start = std::chrono::high_resolution_clock::now();
for(int i = 0; i < 10000; i++) {
mySet.insert(dist(rng)); //using standard insertion funtion
}
end = std::chrono::high_resolution_clock::now();

elapsed = end-start;
std::cout << "Time taken to insert : " << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count() << "s\n";
return 0;
}
Comparison of emplace & insert in a set.

Wow, so insertion did outperform emplace. But why does it happen?

Well the reason is that in order to detect whether a value is already in the container, emplacement implementations typically create a node with the new value so that they can compare the value of this node with existing container nodes. If the value to be added isn’t in the container, the node is linked in. However, if the value is already present, the emplacement is aborted and the node is destroyed, meaning that the cost of its construction and destruction was wasted. Such nodes are created for emplacement functions more often than for insertion functions.

You might say, I began by comparing push_back and emplace_back and demonstrated the difference between emplace and general insertion functions. Well now, I would like to present one more example that is specific to this topic

#include <iostream>
#include <vector>
#include <variant>
#include <chrono>
#include <string>

int main() {
std::vector<std::variant<int, float, std::string>> vec;

// Inserting elements using emplace_back
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; ++i) {
vec.emplace_back(i);
vec.emplace_back(std::to_string(i));
}
auto end = std::chrono::high_resolution_clock::now();
auto emplace_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

// Clearing the vector
vec.clear();

// Inserting elements using push_back
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; ++i) {
vec.push_back(i);
vec.emplace_back(std::to_string(i));
}
end = std::chrono::high_resolution_clock::now();
auto push_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

std::cout << "Time taken for emplace_back: " << emplace_time << " milliseconds" << std::endl;
std::cout << "Time taken for push_back: " << push_time << " milliseconds" << std::endl;

return 0;
}

In this example, I created a vector of std::variant and inserted 1 million elements into the vector using both emplace_back and push_back. I then measured the time taken for each operation and displayed the results.

Output

Well, well, well, looks like those who claim that emplace_back is always faster might need to pump the brakes a bit! 😜

Conclusion -

With current implementations of the standard library, there are situations where, as expected, emplacement outperforms insertion. But, sadly, there are also situations where the insertion functions run faster. Such situations are not easy to characterize, because they depend on the types of arguments being passed, the containers being used, the locations in the containers where insertion or emplacement is requested, etc. So,to determine whether emplacement or insertion runs faster, benchmark them both.

Feel free to ask your questions/doubts in the comments. I will be happy to answer them.

--

--

JustIdeas

https://topmate.io/cppelite -- Engineer by day, jokester by night. I'm always looking for ways to make the world a better place, one laugh at a time!