Consider this example: You have a class A and a container called List<T>
. As shown in the code below, the container is just a wrapper of a std::vector
.
You also have a function called getNObjects
that returns a list containing N instances of the class A.
#include <vector> #include <string> #include <iostream> using namespace std; class A { public: A() { } ~A() { } A(const A& a) { cout << "copy ctor" << endl ; } A& operator=(const A&) { cout << "operator=" << endl; return *this; } }; template <typename T> class List { private: vector<T>* _vec; public: List() : _vec(new vector<T>()) { } ~List() { delete _vec; } List(const List<T>& list) : _vec(new vector<T>(*(list._vec))) { } List<T>& operator=(const List<T>& list) { delete _vec; _vec = new vector<T>(*(list._vec)); return *this; } void add(const T& a) { _vec->push_back(a); } int getCount() const { return _vec->size(); } const T& operator[](int i) const { return (*_vec)[i]; } }; List<A> getNObjects(int n) { List<A> list; A a; for (int i = 0; i< n; i++) list.add(a); cout << "Before returning: ********" << endl; return list; } int main() { List<A> list1; list1 = getNObjects(10); return 0; }
If you run this code, you will see an output similar to this one:
...
...
copy ctor
copy ctor
Before returning: ********
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
The number of calls to the copy constructor is equal to the number of objects in the list.
Why?
Because when the getNObjects()
method returns a list, all attributes of such list are copied (i.e. its inner vector is copied) to the list that receives the result (list1
) and then the local list inside the function is destroyed (the destructor of every object in the list is called). Though this is logically correct, it has a very poor performance because a lot of unnecessary copies and destructions are performed.
In C++11, a new rvalue reference concept has been created to deal with this problem. An rvalue reference is a reference that binds to an rvalue (being lvalue the location of a variable and rvalue the contents of such variable). The rvalues are denoted using the gryph “&&”.
With rvalue references, the programmer can create “move constructors” and “move assignment operators” to improve the performance of their classes when returning or copying objects in contexts similar to my example above.
How does this work? Did you notice I deliberately declared my vector<T> as an integer? My getNObjects
creates a temporary instance of List<T>
. This temporary list contains internally a pointer to a vector
; what would occur if instead of copying every object contained in the std::vector; I would move the pointer of the vector inside the function to the list that receives the result? I would save my program from copy and destroy a lot of objects.
So, that is how the “move constructor” and “move assignment operator” work: They receive an rvalue reference to the list to be moved, they “rip” its data and take ownership of it Taking ownership means that the object where the data were moved will take care of releasing all resources initialized by the original object (that is done simply setting to NULL the pointer to the data in the original object).
This is the implementation of both move constructor and move assignment operator for my example above:
List(List<T>&& list) //move constructor : _vec(list._vec) { list._vec = nullptr; //releasing ownership } List<T>& operator=(List<T>&& list) { delete _vec; _vec = list._vec; list._vec = nullptr; //releasing ownership return *this; }
If you run this program, you will get an output similar to this one:
...
...
copy ctor
copy ctor
Before returning: ********
All the calls to the copy constructor after the “Before returning” entry were avoided.
Nice, isn’t it?
What other use these rvalue references have?
Consider a method to swap two integer numbers:
void my_swap(int& a, int& b) { int c = a; a = b; b = c; }
Quite straightforward. But what if we do not want to swap two integer numbers, but two very heavy objects (two vectors, or two linked lists, or two… whatever):
template <typename T> void my_swap(T& a, T& b) { T c = a; a = b; b = c; }
If the copy constructor of the T class is not fast (like the vector copy constructor, as verified in my last example), my function my_swap
can have a very poor performance.
Look at this code:
#include <iostream> #include <string> using namespace std; class B { private: int _x; public: B(int x) : _x(x) { cout << "ctor" << endl; } B(const B& b) : _x(b._x) { cout << "copy ctor" << endl; } B& operator=(const B& b) { _x = b._x; cout << "operator=" << endl; return *this; } friend ostream& operator<<(ostream& os, const B& b) { os << b._x; return os; } }; template <typename T> void my_swap(T& a, T& b) { T c = a; //copy ctor, possibly slow a = b; //operator=, possibly slow b = c; //operator=, possibly slow } int main() { B a(1); B b(2); my_swap(a, b); cout << a << "; " << b << endl; return 0; }
When executing this program, the output will be:
ctor
ctor
copy ctor
operator=
operator=
2; 1
My class B is quite trivial, but if the copy constructor and the assignment operator are slow; the performance of the my_swap
function will be very sad.
So, to add “move semantics” support to my class B, I must add the move constructor and the move assignment operator to it:
B(B&& b) : _x(b._x) { cout << "move ctor" << endl; } B& operator=(B&& b) { _x = b._x; cout << "move operator=" << endl; return *this; }
If you run the application with this code added; nothing will change: The copy constructor and the assignment operator will be still invoked instead of the “move” ones. Why? Because in my function my_swap
, the compiler does not know if using the copy or the move constructors and assignment operators.
We can rewrite the function my_swap
to tell the compiler to use the move semantics explicitly:
template <typename T> void my_swap(T& a, T& b) { T c = std::move(a); //move ctor, fast a = std::move(b); //move operator=, fast b = std::move(c); //move operator=, fast }
The std::move<T>()
function casts a reference to a rvalue-reference and in this way, the compiler will know that it must to use the move constructor and move assignment operator instead of the copy ones.
The output of my application when modified is:
ctor
ctor
move ctor
move operator=
move operator=
2; 1
All the STL library has been updated to support move semantics. My code was tested using gcc 4.6.
Perfect forwarding is another feature built on top of rvalue references.
Great post!
Just desire to say your article is as amazing. The clarity in your post is just spectacular and i could assume you’re an expert on this subject. Fine with your permission let me to grab your RSS feed to keep up to date with forthcoming post. Thanks a million and please continue the enjoyable work.
Thanks for reading me and for your very nice comments :)