Department of Engineering

IT Services

C++ vectors and copy/move constructors

These examples require C++11 or newer and illustrate some issues with move constructors. They use some features discussed in C++ vector memory and 2D vectors.

In programs, temporary, intermediate variables are often created. For examples, in

  x=3*(a+b)

a variable is created to store the result of a+b then another variable is created to store the value of the expression on the right-hand side (RHS). This variable's value is copied into x then the temporary variables are destroyed. If these variables are big (they may be objects containing images or matrices), creating them may be expensive. Sometimes (in principle at least) it's possible to avoid copying. For instance, in this example once a variable has been created holding the RHS value, it would be nice if somehow the name x could point to the RHS variable, rather than copying the RHS variable into x then destroying it.

"move constructors" arrived with C++11. They offer programmers the chance to exploit these opportunities to speed programs up. This document hopes to show some of the ways that the vector code has been changed to facilitate such exploitation, but doesn't show how to speed code up. Let's first consider this program. It creates a class with 2 constructors which for our purposes merely print messages. Then it adds objects to a vector one at and time

#include <iostream>
#include <vector>
using namespace std;

class A
{
public:
  A() {cout << "A constructor" << endl;}
  A(const A& rhs) {cout << "A copy constructor" << endl;}
};

int main()
{
int main() {
vector <A> v;
cout << "About to push_back 1st element" << endl;
v.push_back(A());

cout << "About to push_back 2nd element" << endl;
v.push_back(A()); 

cout << "About to push_back 3rd element" << endl;
v.push_back(A());

cout << "About to push_back 4th element" << endl;
v.push_back(A());
}

The output is

About to push_back 1st element
A constructor
A copy constructor
About to push_back 2nd element
A constructor
A copy constructor
A copy constructor
About to push_back 3rd element
A constructor
A copy constructor
A copy constructor
A copy constructor
About to push_back 4th element
A constructor
A copy constructor

which shows how much copying happens in programs. When the first element is created, the constructor is called. Then an element is created in the vector by copying. So far so good. The sequence of events involving the 2nd element may be more of a surprise - why is the copy constructor called twice? If we change the program to produce more output we can see why

#include <iostream>
#include <vector>
using namespace std;

class A
{
public:
  A() {cout << "A constructor" << endl;}
  A(const A& rhs) {cout << "A copy constructor" << endl;}
};

int main()
{
int main() {
vector <A> v;
cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 1st element" << endl;
v.push_back(A());

cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 2nd element" << endl;
v.push_back(A()); 

cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 3rd element" << endl;
v.push_back(A());

cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 4th element" << endl;
v.push_back(A());
}

Now the output is

capacity: 0
About to push_back 1st element
A constructor
A copy constructor
capacity: 1
About to push_back 2nd element
A constructor
A copy constructor
A copy constructor
capacity: 2
About to push_back 3rd element
A constructor
A copy constructor
A copy constructor
A copy constructor
capacity: 4
About to push_back 4th element
A constructor
A copy constructor

After adding the first element, the vector was full, so when a second element was added, a new, bigger vector (of size 2) was created behind the scenes. The old vector's element was copied into it, then the new element added, both using the copy constructor. Adding a third element forced new vector space to be created, this time of size 4 (the size goes up in powers of 2), so when the fourth element was added there was no need to create a new vector and copy old values over.

Let's see what happens when we add a move constructor.

#include <iostream>
#include <vector>
using namespace std;

class A
{
public:
  A() {cout << "A constructor" << endl;}
  A(const A& rhs) {cout << "A copy constructor" << endl;}
  A(A&& rhs) {cout << "A move constructor" << endl;}
};

int main()
{
int main() {
vector <A> v;
cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 1st element" << endl;
v.push_back(A());

cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 2nd element" << endl;
v.push_back(A()); 

cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 3rd element" << endl;
v.push_back(A());

cout << "capacity: " << (int) v.capacity() <<  endl;
cout << "About to push_back 4th element" << endl;
v.push_back(A());
}

Now the output is

capacity: 0
About to push_back 1st element
A constructor
A move constructor
capacity: 1
About to push_back 2nd element
A constructor
A move constructor
A copy constructor
capacity: 2
About to push_back 3rd element
A constructor
A move constructor
A copy constructor
A copy constructor
capacity: 4
About to push_back 4th element
A constructor
A move constructor

The vector has used the move constructor in preference to the copy constructor, but not always. Judging by the number of calls, it seems that move is used by push_back but not when new vector storage space is created. If we change the

  A(A&& rhs) {cout << "A move constructor" << endl;}

line to

  A(A&& rhs) noexcept {cout << "A move constructor" << endl;} 

(promising that no exceptions will be thrown by our move constructor) all the copies become moves, and speed-ups might be possible. So what's going on?

The vector code tries to use the move constructor if possible in the hope that it will improve efficiency, but the vector routines have tight specifications. In particular, there are circumstances where a move constructor's only used if it doesn't throw exceptions.

To summarise -

  • Note that vectors do quite a lot of copying behind the scenes. You can reduce that by setting the vector to its maximum size immediately
  • The standard algorithms offer opportunities to exploit move constructors, but it's not a topic for beginners.

p.s. I don't know why in the last sample of program output the move constructor seems to be called before the copy constructor.