移动构造函数


  • 测试环境

    vs2015

    C++11

    win10 64位

C++11之前的局限


在介绍移动构造函数之前先看看我们面临的问题,先看代码示例:

	class CMoveSemantic
	{
	public:
		CMoveSemantic();
		explicit CMoveSemantic(int k);
		CMoveSemantic(int k, char ch);
		CMoveSemantic(const CMoveSemantic& objMoveSemantic);
		~CMoveSemantic();

	public:
		CMoveSemantic operator+(const CMoveSemantic&f) const;
		void ShowData() const;

	private:
		void ShowObject()const;

	private:
		int n;
		char * pc;
		static int ct;
	};

	int CMoveSemantic::ct = 0;

	CMoveSemantic::CMoveSemantic()
	{
		++ct;
		n = 0;
		pc = nullptr;
		cout << "default constructor called;number of objects: " << ct << endl;
		ShowObject();
		cout << endl;
	}

	CMoveSemantic::CMoveSemantic(int k):n(k)
	{
		++ct;
		cout << "int constructor called;number of objects: " << ct << endl;
		pc = new char[n];
		ShowObject();
		cout << endl;
	}

	CMoveSemantic::CMoveSemantic(int k, char ch):n(k)
	{
		++ct;
		cout << "int char constructor called;number of objects: " << ct << endl;
		pc = new char[n];
		for (int i = 0;i < n;i++)
		{
			pc[i] = ch;
		}
		ShowObject();
		cout << endl;
	}

	CMoveSemantic::CMoveSemantic(const CMoveSemantic& objMoveSemantic):n(objMoveSemantic.n)
	{
		++ct;
		cout << "copy const called;number of objects: " << ct <<" Src object address: "<<(void *)(&objMoveSemantic)<< endl;
		pc = new char[n];
		for (int i = 0; i < n; i++)
		{
			pc[i] = objMoveSemantic.pc[i];
		}
		ShowObject();
		cout << endl;
	}
	CMoveSemantic::~CMoveSemantic()
	{
		cout << "destructor called;objects left: " << --ct << endl;
		cout << "deleted object;\n";
		ShowObject();
		delete []pc;
		cout << endl;
	}
	CMoveSemantic CMoveSemantic::operator+(const CMoveSemantic& f)const
	{
		cout << "Enter operator+()\n";
		CMoveSemantic tmp = CMoveSemantic(n + f.n);
		for (int i = 0; i < n; i++)
		{
			tmp.pc[i] = pc[i];
		}
		for (int i = n; i < tmp.n; i++)
		{
			tmp.pc[i] = f.pc[i-n];
		}
		cout << "tmp object address: " << (void *)(&tmp) << endl;;
		cout << "leaving operator+()" << endl;
		cout << endl;
		return tmp;
	}
	void CMoveSemantic::ShowObject() const
	{
		cout << "Number of element: " << n;
		cout << " Data address: " << (void*)pc << endl;
		cout << endl;
	}

	void CMoveSemantic::ShowData()const
	{
		if (n == 0)
		{
			cout << "(object empty)";
		}
		else
		{
			for (int i = 0;i < n;i++)
			{
				cout << pc[i];
			}
		}
		cout << endl;
	}

调用代码:

	{
		CMoveSemantic one(10, 'x');
		CMoveSemantic three(20, 'o');
		CMoveSemantic four(one + three);
		cout <<"--------"<< endl;
	}

我们猜猜`CMoveSemantic four(one + three);`这段代码调用哪些构造函数

1. 首先函数operator+里`CMoveSemantic(n + f.n)`调用构造函数创建临时对象

2. `CMoveSemantic tmp = CMoveSemantic(n + f.n);`调用拷贝构造

3. `return tmp;`调用拷贝构造创建临时对象

4. `CMoveSemantic four(one + three);`调用拷贝构造

我们看看输出:

	Enter operator+()
	int constructor called;number of objects: 3
	Number of element: 30 Data address: 00ECA0F0


	tmp object address: 00AFF630
	leaving operator+()

	copy const called;number of objects: 4 Src object address: 00AFF630
	Number of element: 30 Data address: 00ECA780


	destructor called;objects left: 3
	deleted object;
	Number of element: 30 Data address: 00ECA0F0

从输出上看出,只调用一次构造函数,一次拷贝构造函数。这里编译器做了优化,关于构造函数的编译器优化可以参考这篇[文章][program_transformation_semantic],我们先不去
关心编译器如何做的优化,这里我们可以看出变量tmp分配的内存随后被释放了,而且这块内存对于tmp没有任何意义,那么它所分配、释放的过程是被浪费掉的,那么我们想把tmp分配的内存让新对象拥有怎么办呢?传统的拷贝构造又做不到,这时移动构造函数来了,由于在移动构造函数里会对原对象做操作,所以参数不能有const,这个参数是个右值引用(关于左值、右值可以参考这篇[文章][move_right_reference]),而且以后也不能对这个实参做操作了,它已经无意义了。我们针对上面的代码增加移动构造函数

	CMoveSemantic(CMoveSemantic&& objMoveSemantic);
	CMoveSemantic::CMoveSemantic(CMoveSemantic&& objMoveSemantic):n(objMoveSemantic.n)
	{
		++ct;
		cout << "move constructor called;number of objects: " << ct << endl;
		pc = objMoveSemantic.pc;
		objMoveSemantic.pc = nullptr;
		objMoveSemantic.n = 0;
		ShowObject();
	}

同样的调用方式,输出:

	Enter operator+()
	int constructor called;number of objects: 3
	Number of element: 30 Data address: 0094EE68


	tmp object address: 006FFDB0
	leaving operator+()

	move constructor called;number of objects: 4
	Number of element: 30 Data address: 0094EE68

	destructor called;objects left: 3
	deleted object;
	Number of element: 0 Data address: 00000000

上面的移动构造函数的实现就是把那个无意义的对象tmp分配的内存转移到新对象,同时无意义的对象tmp里指向这块内存的指针被设置为nullptr,从而省去了重复分配的工作提高了效率

上面的函数operator+返回的是个临时对象,而临时对象是个右值,我们知道如果实参为右值,const引用形参将指向一个以这个实参为初始化物的临时变量,从输出可以看出这个临时对象指向变量tmp,所以在没有引入移动构造函数时调用了拷贝构造函数,引入移动构造函数后调用了移动构造函数,但是平时我们有不是临时变量的情况即参数是个左值,左值无法绑定到右值,这时std::move出生了,而关于std::move和右值引用可以参考这篇[文章][move_forward]。还有移动赋值运算符,移动赋值运算符的实现:

	CMoveSemantic& operator=(CMoveSemantic&& objMoveSemantic);
	CMoveSemantic& operator=(const CMoveSemantic& objMoveSemantic);

	CMoveSemantic& CMoveSemantic::operator=(CMoveSemantic&& objMoveSemantic)
	{
		cout << "move assignment called;"<< endl;
		if (this == &objMoveSemantic)
		{
			return *this;
		}
		delete[]pc;
		n = objMoveSemantic.n;
		pc = objMoveSemantic.pc;
		objMoveSemantic.pc = nullptr;
		objMoveSemantic.n = 0;
		ShowObject();
		return *this;
	}

	CMoveSemantic& CMoveSemantic::operator=(const CMoveSemantic& objMoveSemantic)
	{
		cout << "copy assignment called;" << endl;
		if (this == &objMoveSemantic)
		{
			return *this;
		}
		delete[]pc;
		n = objMoveSemantic.n;
		pc = new char[n];
		for (int i = 0; i < n;i++)
		{
			pc[i] = objMoveSemantic.pc[i];
		}
		return *this;
	}

移动构造函数和移动赋值函数


C++11增加了两个特殊的成员函数:

  1. 移动构造函数

  2. 移动赋值运算符

这两个函数的生成规则类似复制构造函数、复制赋值函数,也只是在需要的时候才生成。这两个函数都会移动构造、移动赋值它的基类部分,移动操作并不能保证真的发生,对于那些没有提供移动操作的型别只能通过复制操作来实现。所以按成员移动是由两部分组成, 一部分是在支持移动操作的成员上执行移动操作,另一部分是在不支持移动操作的成员上执行复制操作

在C++98中两种复制操作彼此独立,即声明了一个并不会阻止编译器生成另外一个,例如声明了一个复制构造函数并未声明复制赋值函数,而编写了要求有复制赋值的代码,此时编译器会为我们生成复制赋值函数,这样的规则在C++11中同样成立。

而移动操作的这两种函数并不彼此独立,即声明了其中一个,就会阻止编译器生成另外一个,也就是声明一个移动构造函数会阻止编译器生成移动赋值函数,而声明一个移动赋值函数会阻止编译器生成移动构造函数。 其实这样做的理由是:例如当我们编写了移动构造函数说明我们有一定自己的意图,而当编写了调用移动赋值函数的代码说明这种意图同样应该应用于移动赋值函数。按照这个初衷那么一旦声明了复制操作,那么这个类也不能生成移动操作了,反之亦然, 当声明了移动操作,那么这个类也不能生成复制操作了,废除的方式是以delete的方式,还是用这篇文章的例子,我们把复制构造函数注释掉,增加如下代码:

{
	CMoveSemantic one(10, 'x');
	CMoveSemantic five(one);
}

编译报错:

无法引用 函数 "CMoveSemantic::CMoveSemantic(const CMoveSemantic &)" (已隐式声明) -- 它是已删除的函数

C++其实一直有个三大定律,如果声明了复制构造函数、复制赋值函数、析构函数的任何一个,那么就得同时声明所有这三个。理由和上面提到差不多,其思想就是如果有改写复制操作的需求,通常意味着该类可能要执行某种资源管理:

  1. 在一种复制操作函数中进行资源管理,也极有可能在另外一个种复制操作中进行资源管理
  2. 该类的析构函数也会参与到资源管理

这个三大定律并没有强硬的写入标准,所以即使用户声明了析构函数,也不会影响编译器生成复制操作。基于这样的思想也推动了C++11中这样的规定:只要用户声明了析构函数就不会生成移动操作。

C++11的移动操作的生成条件:

  • 该类未声明任何复制操作
  • 该类未声明任何移动操作
  • 该类未声明任何析构函数

结合上面这些规则,如果期望默认的析构、复制操作、移动操作都正确,但是由于有这么多的生成条件限制,并不能保证编译器给我们生成,其实我们可以通过C++11的default来达到这样的目的。例如:

class CWrapper {
public:
	virtual ~CWrapper() = default;
 
	CWrapper(CWrapper&&) = default;
	CWrapper& operator=(CWrapper&&) = default;
 
	CWrapper(const CWrapper&) = default;
	CWrapper& operator=(const CWrapper&) = default;
private:
	std::map<int,std::string> m_values;
};

对于移动操作设置default可以改变外部调用移动操作而被决议到复制操作函数这样的行为,从而真正调用移动操作,从而调用类内部数据的移动操作,STL内很多容器都提供了移动操作, 对于一个使用复制操作和一个使用移动操作的情况,表现出来的性能可能天壤之别


  • 最后

    C++11增加了两个特殊的成员函数:

    1. 移动构造函数

    2. 移动赋值运算符

    如果没有提供移动构造函数而代码又需要它,将使用复制构造函数

    复制构造函数仅当类中不包含用户显式声明的复制构造函数时才生成,如果该类声明了移动操作函数则复制构造函数将被删除,就像上面的报错信息一样

    复制赋值函数仅当类中不包含用户显式声明的复制赋值函数才生成,如果该类声明了移动操作 则复制赋值运算符浆被删除。

    在已经存在显式声明的析构函数的条件下,生成复制操作已经成为被废弃的行为

    移动操作仅当类中未包含用户显示声明的复制操作、移动操作和析构函数才生成

    成员函数模板在任何情况下都不会抑制特种成员函数的生成