C++ vector has these two very special functions. Both of them insert object at the end position of vector. Therefore, fundamental topic is “emplace_back vs push_back”.
How push_back works ?
The push_back function of cpp vector accepts an argument object of vector type T. Therefore, the user can first construct an object and then pass this object as argument. However, when this argument is an L-value object, then push_back will invoke copy constructor. This will make a copy of object and pushes that copy in vector. But if the argument is R-value, then push_back calls move constructor of type T to enable taking ownership of R-value in a second object. Overall, process calls the constructor two times for using vector push_back.
Example: suppose “MainFunda” is a simple user-defined class
#include <iostream> //main header
#include <vector> //std vector
using namespace std;//namespace
class MainFunda
{
std::string name;
public:
MainFunda() : name("basic")
{
cout << "Default Constructor: "
<< name
<< endl;
}
MainFunda(std::string s) : name(s)
{
cout << "Parametrized Constructor: "
<< name
<< endl;
}
MainFunda(const MainFunda& rhs) : name(rhs.name)
{
cout << "Copy Constructor: From "
<< name << endl;
}
MainFunda(MainFunda&& rhs) noexcept :
name(std::move(rhs.name))
{
cout << "Move Constructor : From "
<< name
<< endl;
}
~MainFunda()
{
cout << "Destructor : "
<< name
<< endl;
}
};
int main()
{
std::vector<MainFunda> vtest;
vtest.reserve(2);
MainFunda a1("one");
vtest.push_back(a1); //Pushing an L-value
vtest.push_back(MainFunda("two")); //Pushing an R-value
return 0;
}
The L-value case, will produce the output :
Parametrized Constructor: one <--Before push_back
Copy Constructor: From one <--During push_back
Destructor : one <-- When a1 goes out-of-scope
Destructor : one <--When vector goes out-of-scope
For R-value case, the output may look like
Parametrized Constructor: two <--During push_back
Move Constructor : From two <--During push_back
Destructor : <--During push_back
Destructor : two <--When vector goes out-of-scope
How emplace_back works ?
The emplace_back function of vector can accept an argument which will be just like push_back. Otherwise, it can directly accepts those arguments which the constructor of object needs. After this, it uses those arguments to create an object directly on the container. For instance, this can be done by calling relevant constructor (copy constructor or move constructor). Therefore, the vector emplace_back function calls the constructor only one time which is more efficient.
int main()
{
std::vector<MainFunda> vtest;
vtest.reserve(3);
MainFunda a1("one");
vtest.emplace_back(a1); //Pushes L-value
vtest.emplace_back(MainFunda("two"));
//Pushes R-value object
vtest.emplace_back("three"); //Pushes argument of A
return 0;
}
The L-value case will produce an output. Besides, this i similar to push_back,
Parametrized Constructor: one <--Before emplace_back
Copy Constructor: From one <--During emplace_back
Destructor : one <-- When a1 goes out-of-scope
Destructor : one <--When vector goes out-of-scope
The R-value case output will also be similar to push_back case.
Parametrized Constructor: two <--During emplace_back
Move Constructor : From two <--During emplae_back
Destructor : <--During emplace_back
Destructor : two <--When vector goes out-of-scope
The third case when pushing argument of the class shows following output
Parametrized Constructor: three <--During emplace_back
Destructor : three <--When vector goes out-of-scope
Sample Implementation of C++ vector push_back and emplace_back
The general implementation of stl vector is easily available in sources. However, a dummy raw implementation, is available here. In particular, it will help to understand the semantics of using vectors in c++.
template<typename T>
class fake_vector
{
public:
void dummy_push_back(T&& item); //R-value param
void dummy_push_back(T& item); //L-value param
template<typename ...TArg>
void dummy_emplace_back(TArg&&... arg);
};
template<typename T>
void fake_vector<T>::dummy_push_back(T&& item)
{
cout << "push_back(R-Value): ";
cout << item << endl;
}
template<typename T>
void fake_vector<T>::dummy_push_back(T& item)
{
cout << "push_back(L-Value): ";
cout << item << endl;
}
template<typename T>
template<typename ...TArg>
void fake_vector<T>::dummy_emplace_back(TArg&&... arg)
{
cout << "emplace_back : ";
dummy_push_back( T(std::forward<TArg>(arg)...) );
}
ostream& operator<<(ostream& os, A& rhs)
{
os << rhs.getName();
return os;
}
Explanation:
Firstly, push_back has 2 variations for accepting L-value & R-value C++ reference type objects.
Secondly, emplace_back accepts variable arguments. It then perfectly forwards them to constructor of T.
template<typename T>
template<typename ...TArg>
void fake_vector<T>::dummy_emplace_back(TArg&&... arg)
{
cout << "emplace_back : ";
dummy_push_back( T(std::forward<TArg>(arg)...) );
}
There will be 3 cases, where perfect forwarding works, for instance, when
- L-value object passed
- R-value object passed
- Direct argument passed
L-value object
a8 is an L-value.
MainFunda a8("eight");
vs.dummy_emplace_back(a8);
Therefore, this will deduce universal reference TArg&& to become an L-value MainFunda&. Hence, std::forward(MainFunda&) will return an L-value object “MainFunda&”. This means, this argument will invoke copy constructor of T (i.e. MainFunda).
T(std::forward<TArg>(arg)...) ==> MainFunda(MainFunda& arg)
The above input will generate an output:
Parametrized Constructor: eight
emplace_back : Copy Constructor: From eight
push_back(R-Value): eight
Destructor : eight
Destructor : eight
R-value object
MainFunda(“seven”) is an R-value
vs.dummy_emplace_back(MainFunda("seven"));
This call will deduce universal reference TArg&& to become an R-value, MainFunda&&. Hence, std::forward(MainFunda&&) will return an R-value object “MainFunda&&”.
This means, this argument will invoke move constructor of T (i.e. MainFunda)
T(std::forward<TArg>(arg)...) ==> MainFunda(MainFunda&& arg)
The above input will generate an output
Parametrized Constructor: seven
emplace_back : Move Constructor : From seven
push_back(R-Value): seven
Destructor : seven
Destructor :
Direct Argument
“six” is an argument of type char*. This will become std::string when it goes to parametrized constructor of MainFunda.
vs.dummy_emplace_back("six");
This will deduce universal reference TArg&& to become R-value “char* && arg”. Hence, std::forward(char*&& ) will return an R-value “char* &&arg”. Certainly, this is convertible to std::string type.
This means, the argument will invoke Parametrized constructor.
T(std::forward<TArg>(arg)...) ==> MainFunda(std::string arg)
The above input will generate an output
emplace_back : Parametrized Constructor: six
push_back(R-Value): six
Destructor : six
Passing zero argument to emplace_back
When the caller do not provides any argument, then emplace_back will invoke default constructor of vector type T. The class must have a default constructor, i.e. which do not take any argument.
vs.dummy_emplace_back();
The output is:
emplace_back : Default Constructor: basic
push_back(R-Value): basic
Destructor : basic