Move Constructor & Move Assignment Operator in C++ 11

C++11 defines two new functions in service of move semantics: a move constructor, and a move assignment operator. Whereas the goal of the copy constructor and copy assignment is to make a copy of one object to another, the goal of the move constructor and move assignment is to move ownership of the resources from one object to another (which is much less expensive than making a copy).

Defining a move constructor and move assignment work analogously to their copy counterparts. However, whereas the copy flavours of these functions take a const l-value reference parameter, the move flavours of these functions use non-const r-value reference parameters.

C++98/03

All the member function created by compiler for a class:

  1. the compiler will generate a default constructor for you unless you declare any constructor of your own.
  2. the compiler will generate a copy constructor for you unless you declare your own.
  3. the compiler will generate a copy assignment operator for you unless you declare your own.
  4. the compiler will generate a destructor for you unless you declare your own.

C++11 adds the following rules, which are also true for C++14:

  • The compiler generates the move constructor if
    • there is no user-declared copy constructor, and
    • there is no user-declared copy assignment operator, and
    • there is no user-declared move assignment operator and
    • there is no user-declared destructor,
    • it is not marked deleted,
    • and all members and bases are moveable.
  • Similarly for move assignment operator, it is generated if
    • there is no user-declared copy constructor, and
    • there is no user-declared copy assignment operator, and
    • there is no user-declared move constructor and
    • there is no user-declared destructor,
    • it is not marked deleted,
    • and all members and bases are moveable

Note that the default move constructor and move assignment do the same thing as the default copy constructor and copy assignment (make copies, not do moves)

Rule: If you want a move constructor and move assignment that do moves, you’ll need to write them yourself.

The key insight behind move semantics in move constructor and move assignment operator

If we construct an object or do an assignment where the argument is an l-value, the only thing we can reasonably do is copy the l-value. We can’t assume it’s safe to alter the l-value, because it may be used again later in the program. If we have an expression “a = b”, we wouldn’t reasonably expect b to be changed in any way.

However, if we construct an object or do an assignment where the argument is an r-value, then we know that r-value is just a temporary object of some kind. Instead of copying it (which can be expensive), we can simply transfer its resources (which is cheap) to the object we’re constructing or assigning. This is safe to do because the temporary will be destroyed at the end of the expression anyway, so we know it will never be used again!

C++11, through r-value references, gives us the ability to provide different behaviours when the argument is an r-value vs an l-value, enabling us to make smarter and more efficient decisions about how our objects should behave.

Example:

Let’s take an example of class that use dynamic allocated memory and performance improvement without/with using move constructor and move assignment operator.

#include<iostream>
#include <chrono>

using namespace std;

class resource
{
	int * data;
	int length;
public:

	int getlength()
	{
		return length;
	}

	int* getdata()
	{
		return data;
	}

	resource():length(0), data(nullptr)
	{
		cout << "Resource constructor initialize\n";
	}

	resource(int len)
	{
		length = len;
		data = new int[len];
		cout << "Resource parameter constructor initialize\n";
	}

	~resource()
	{
		if(data != nullptr)
			delete data;
		cout << "Resource destroyed \n";
	}

	resource(const resource & r)
	{
		cout << "copy Constructor called\n";
		length = r.length;
		data = new int[r.length];
		
		for (int i = 0; i < length; i++)
		{
			data[i] = r.data[i];
		}
	}
	
	resource & operator =(const resource & r)
	{
		cout << "Assignment Operator called\n";
		if (this != &r)
		{
			delete[] data;
			length = r.length;

			data = new int[length];

			for (int i = 0; i < r.length; i++)
			{
				data[i] = r.data[i];
			}
		}
		return *this;
	}
};

resource getResource()
{
	resource res(100000);

	for (int i = 0; i < res.getlength(); i++)
	{
		res.getdata()[i] = i * 2;
	}

	return res;
}

class Timer
{
private:
	// Type aliases to make accessing nested type easier
	using clock_t = std::chrono::high_resolution_clock;
	using second_t = std::chrono::duration<double, std::ratio<1> >;

	std::chrono::time_point<clock_t> m_beg;

public:
	Timer() : m_beg(clock_t::now())
	{
	}

	void reset()
	{
		m_beg = clock_t::now();
	}

	double elapsed() const
	{
		return std::chrono::duration_cast<second_t>(clock_t::now() - m_beg).count();
	}
};

void main()
{
	Timer t;
	resource r1(1000);
	
	r1 = getResource();

	resource r2 = getResource();

	std::cout << t.elapsed();
}

Output:

Resource parameter constructor initialise
Resource parameter constructor initialise
copy Constructor called
Resource destroyed
Assignment Operator called
Resource destroyed
Resource parameter constructor initialise
copy Constructor called
Resource destroyed

Resource destroyed

Resource destroyed

On one of the author’s machines, in release mode, this program executed in 0.0189463 seconds.

Now let’s run the same program again, replacing the copy constructor and copy assignment with a move constructor and move assignment.

#include<iostream>
#include <chrono>

using namespace std;

class resource
{
	int * data;
	int length;
public:

	int getlength()
	{
		return length;
	}

	int* getdata()
	{
		return data;
	}

	resource():length(0), data(nullptr)
	{
		cout << "Resource constructor initialize\n";
	}

	resource(int len)
	{
		length = len;
		data = new int[len];
		cout << "Resource parameter constructor initialize\n";
	}

	~resource()
	{
		if(data != nullptr)
			delete data;
		cout << "Resource destroyed \n";
	}

	resource(const resource & r)
	{
		cout << "copy Constructor called\n";
		length = r.length;
		data = new int[r.length];
		
		for (int i = 0; i < length; i++)
		{
			data[i] = r.data[i];
		}
	}

	resource(resource && r)
	{
		cout << "Move Constructor called\n";
		length = r.length;
		data = r.data;

		r.length = 0;
		r.data = nullptr;
	}
	
	resource & operator =(const resource & r)
	{
		cout << "Assignment Operator called\n";
		if (this != &r)
		{
			delete[] data;
			length = r.length;

			data = new int[length];

			for (int i = 0; i < r.length; i++)
			{
				data[i] = r.data[i];
			}
		}
		return *this;
	}

	resource & operator=(resource && r)
	{
		cout << "Move assignment Operator called\n";

		if (this != &r)
		{
			delete[] data;

			length = r.length;
			r.length = 0;

			data = r.data;
			r.data = nullptr;
		}

		return *this;
	}
};

resource getResource()
{
	resource res(100000);

	for (int i = 0; i < res.getlength(); i++)
	{
		res.getdata()[i] = i * 2;
	}

	return res;
}

class Timer
{
private:
	// Type aliases to make accessing nested type easier
	using clock_t = std::chrono::high_resolution_clock;
	using second_t = std::chrono::duration<double, std::ratio<1> >;

	std::chrono::time_point<clock_t> m_beg;

public:
	Timer() : m_beg(clock_t::now())
	{
	}

	void reset()
	{
		m_beg = clock_t::now();
	}

	double elapsed() const
	{
		return std::chrono::duration_cast<second_t>(clock_t::now() - m_beg).count();
	}
};

void main()
{
	Timer t;
	resource r1(1000);
	
	r1 = getResource();

	resource r2 = getResource();

	std::cout << t.elapsed();
}

Output:

Resource parameter constructor initialise
Resource parameter constructor initialise
Move Constructor called
Resource destroyed
Move assignment Operator called
Resource destroyed
Resource parameter constructor initialise
Move Constructor called
Resource destroyed

On the same machine, this program executed in 0.012287 seconds.

Comparing the runtime of the two programs, 0.012287 / 0.0189463 = 64.8%.The move version was almost 35 % faster!

  • 23
    Shares