移动语义和右值引用

右值引用:

c++的传统引用(现在成为左值引用)使得时标符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可以获取其地址。

C++11新增了右值引用,用&&表示。右值引用可以关联到右值,即可出现在赋值表达式的右边,但不能对其应用地址运算符的值。右值包括字面常量、如x+y等表达式以及返回值的函数:

int x =10;

int y = 23;

int &&r1= 13;

int &&r2 = x+y;

double && r3 = std::sqrt(2.0);

注意:r2关联到的是当时计算x+y得到的结果。也就是说,r2关联到的是23,即使以后修改了x或y,也不会影响到r2。

下面代码简要演示了关于右值引用的要点:

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

inline double f(double tf)
{
	return 5.0*(tf - 32.0) / 9.0;
}

int main()
{
	using namespace std;

	double tc = 21.5;
	double && rd1 = 7.07;
	double && rd2 = 1.8*tc + 32.0;
	double && rd3 = f(rd2);
	cout << "tc value and address: " << tc << ", " << &tc << endl;
	cout << "rd1 value and address: " << rd1 << ", " << &rd1 << endl;
	cout << "rd2 value and address: " << rd2 << ", " << &rd2 << endl;
	cout << "rd3 value and address: " << rd3 << ", " << &rd3 << endl;
	cin.get();

	return 0;
}

输出:

tc value and address: 21.5, 008FF910
rd1 value and address: 7.07, 008FF8F4
rd2 value and address: 70.7, 008FF8D8
rd3 value and address: 21.5, 008FF8BC
 

引入右值引用的主要目的之一是实现移动语义。

移动语义:

为什么要移动语义:

vector<string> allcaps(const vector<string> &vs)
{
    vector<string> temp;
    ....
    ........
    return temp;
}

接下来,以这种方式使用它:

vector<string> vstr;

vector<string> vstr_copy1(vstr);                       //#1
vector<string> vstr_copy2(allcaps(vstr));              //#2

深入探索这段代码,发现allcaps()创建了对象temp,该对象管理着20000000个字符;vector和string的复制构造函数创建这20000000个字符的副本,然后程序删除allcaps()返回的临时对象。这里的重点是:做了大量的无用功!因为临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr_copy2,不是更好吗?这类似于在计算机中移动文件的情形,实际文件还留在原来的地方,只修改记录。这种方法被称为移动语义。

实现移动语义:

下来我们来说说如何实现移动语义。要实现移动语义,需要采用某种方式,让编辑器知道什么时候需要复制,什么时候不需要。

(1)常规的复制构造函数。使用const左值引用作为参数,这个引用关联到左值实参。

(2)移动构造函数。使用右值引用作为实参,该引用关联到右值实参。

复制构造函数构造函数可以进行深度复制,而移动构造函数只调整记录。所谓的“窃取”资源。

实例:

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

class Useless
{
private:
	int n;
	char *pc;
	static int ct;
	void ShowObject()const;

public:
	Useless();
	explicit Useless(int k);
	Useless(int k, char ch);
	Useless(const Useless &f);
	Useless(Useless && f);
	~Useless();

	Useless operator+(const Useless & f)const;

	Useless & operator=(const Useless & f);
	
	Useless & operator=(Useless && f);

	void ShowData()const;

};

int Useless::ct = 0;

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

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

Useless::Useless(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();
}

Useless::Useless(const Useless & f) :n(f.n)
{
	++ct;
	//cout << "copy const called; number of objects: " << ct << endl;
	pc = new char[n];
	for (int i = 0; i < n; i++)
		pc[i] = f.pc[i];
	//ShowObject();
}

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

Useless::~Useless()
{
	cout << "destructor called; objects left: " << --ct << endl;
	cout << "deleted object:\n";
	ShowObject();
	delete[] pc;
}

Useless & Useless::operator=(const Useless & f)
{
	std::cout << "copy assigment operator called:\n";
	if (this == &f)
		return *this;

	delete[]pc;

	n = f.n;
	pc = new char[n];
	for (int i = 0; i < n; i++)
		pc[i] = f.pc[i];

	return *this;
}

Useless & Useless::operator=(Useless && f)
{
	std::cout << "move assignment operator called:\n";
	if (this == &f)
		return *this;
	delete [] pc;

	n = f.n;
	pc = f.pc;
	f.n = 0;
	f.pc = nullptr;

	return *this;
}

Useless Useless::operator+(const Useless & f)const
{
	Useless temp = Useless(n + f.n);
	for (int i = 0; i < n; i++)
		temp.pc[i] = pc[i];
	for (int i = n; i < temp.n; i++)
		temp.pc[i] = f.pc[i - n];

	cout << "temp object:\n";
	cout << "Leaving operator+()\n";

	return temp;
}

void Useless::ShowObject()const
{
	std::cout << "Number of elements: " << n;
	std::cout << " Data address: " << (void *)pc << std::endl;
}

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

	std::cout << std::endl;
}

实例选自于c++ primer plus

调用函数:

版本1:

int main()
{
	{
		Useless one(10, 'x');
		Useless two = one;
		Useless three(20, 'o');
		Useless four(one + three);
		cout << "object one: ";
		one.ShowData();
		cout << "object two: ";
		two.ShowData();
		cout << "object three: ";
		three.ShowData();
		cout << "object four: ";
		four.ShowData();
	}

	return 0;

}

版本2:

int main()
{
	{
		Useless one(10, 'x');
		Useless two = one + one;
		cout << "object one: ";
		one.ShowData();
		cout << "object two: ";
		two.ShowData();
		Useless three, four;
		cout << "three = one\n";
		three = one;
		cout << "now object three = ";
		three.ShowData();
		cout << "and object one = ";
		one.ShowData();
		cout << "four = one + two\n";
		four = one + two;
		cout << "now object four = ";
		four.ShowData();
		cout << "four = move(one)\n";
		four = move(one);
		cout << "now object four = ";
		four.ShowData();
		cout << "and object one = ";
		one.ShowData();

	}
}

先来看看移动构造函数:

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

他让pc指向现有的数据,获取这些数据的所有权。此时,pc和f.pc指向相同的数据,调用析构函数会带来麻烦,因为程序不可能delete两次。为避免这种问题,随后将原来的指针(f.pc)设置为空。这种夺取所有权的方式常被称为“窃取”。

由于要修改原始的对象数据,所有这要求不能再函数参数声明中,使用const。

在版本一中,下面这个语句调用了这个移动构造函数:

Useless four(one + three);

表达式one+three调用了Useless::operator+(),而右值引用f将关联到该方法返回的临时对象。

版本一调用结果:

Useless::operator+()中创建的对象的数据地址,与four存储的数据地址相同(都是007C7C40),其中对象four是由移动复制构造函数创建的。另外,创建four对象之后,为临时对象调用了析构函数。

移动构造函数发生条件:

要让移动语义发生,需要两个步骤,首先,右值引用让编译器知道何时可使用移动语义:

Useless two = one;     //Useless::Useless(const Useless &)

Useless four(one + three);    //Useless::Useless(Useless &&)

对象one是左值,而表达式one+three是右值,与右值引用匹配。

第二步骤是:编写移动构造构造函数。

那么问题来了:上述代码如果没有移动构造函数呢?结果会怎样?其实也很简单,稍微想想就是答案。我去掉移动构造函数之后,输出如下:

这里我跟书上的结果不太一样,应该是编辑器优化的问题,自动消除额外的一些工作,具体在c++ primer plus 808页。但是仍然可以反映出问题,最后调用的是拷贝构造函数。

版本二:

多了使用移动复制运算符,以及move。

Useless & Useless::operator=(const Useless & f)
{
	std::cout << "copy assigment operator called:\n";
	if (this == &f)
		return *this;

	delete[]pc;

	n = f.n;
	pc = new char[n];
	for (int i = 0; i < n; i++)
		pc[i] = f.pc[i];

	return *this;
}

move:上述我们说到,移动构造函数和移动复制运算符,参数使用右值。那么如果要让她使用左值怎么办呢?

我们可以使用static_cast<>将对象强制转为Useles&&,或者更简便的方式-使用Move().

版本二输出如下:

本文实例选自c++ primer plus。简单的说明了移动语义和右值引用,并没有高级的写法,比如考虑异常等信息。

猜你喜欢

转载自blog.csdn.net/Rage_/article/details/82957381
今日推荐