cpp►C++11右值引用移动语义移动构造函数移动赋值运算符

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cpp►C++11右值引用移动语义移动构造函数移动赋值运算符相关的知识,希望对你有一定的参考价值。

右值引用

传统的C++引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。修饰符const的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址。

C++11新增了右值引用,这是使用&&表示的。右值引用可以关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。
右值包括字面常量(C-风格字符串除外,它表示地址)、诸如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关联到的是33,即使以后修改了x或y,也不会影响到r2。

将右值关联到右值引用会导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于13,但可将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。

下面是一个简短示例,演示了上述有关右值引用的要点:

#include <iostream>
inline double f(double tf) { return (tf - 32.0) / 1.8; }
int main() {
	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;
	return 0;
}
 tc value and address: 21.5, 00D3F9E4
rd1 value and address: 7.07, 00D3F9C8
rd2 value and address: 70.7, 00D3F9AC
rd3 value and address: 21.5, 00D3F990

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

移动语义

先来看看C++11之前的复制过程。假设有如下代码:

vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
...
vector<string> vstr_copy1(vstr);// make vstr_copy1 a copy of vstr

vector和string类都使用动态内存分配,因此它们必须定义使用某种new版本的复制构造函数。为初始化对象vstr_copy1,复制构造函数vector<string>将使用new给20000个string对象分配内存,而每个string对象又将调用string的复制构造函数,该构造函数使用new为1000个字符分配内存。接下来,全部20000000个字符都将从vstr控制的内存中复制到vstr_copy1控制的内存中。这里的工作量很大,但只要妥当就行。

以上确实妥当吗?有时答案是否定的。
例如,假设有一个函数,它返回一个vector<string>对象:

vector<string> allcaps(const vector<string>& vs) {
	vector<string> temp;
	// code that stores an all-uppercase version of vs in temp
	return temp;
}

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

vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
vector<string> vstr_copy1(vstr);          // #1
vector<string> vstr_copy2(allcaps(vstr)); // #2

allcaps()创建了对象temp,该对象管理着20000000个字符;vector和string的复制构造函数创建这20000000个字符的副本,然后程序删除allcaps()返回的临时对象。这里的要点是,做了大量的无用功。

考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr_copy2,不是更好吗?也就是说,不将20000000个字符复制到新地方,再删除原来的字符,而将字符留在原来的地方,并将vstr_copy2与之相关联。

这类似于在计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录。这种方法被称为移动语义(move semantics)
有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录。

要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。
可以定义两个构造函数。其中一个是常规复制构造函数,它使用const左值引用作为参数,如语句#1中的vstr;另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句#2中allcaps(vstr)的返回值。
复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。

一个移动示例
#include <iostream>
using namespace std;
// interface
class Useless {
private:
	int n;// number of elements
	char* pc;// pointer to data
	static int ct;// number of objects
	void ShowObject() const;
public:
	Useless();
	explicit Useless(int k);
	Useless(int k, char ch);
	Useless(const Useless& f);// regular copy constructor
	Useless(Useless&& f);// move constructor
	~Useless();
	Useless operator+(const Useless& f) const;
	// need operator=() in copy and move versions
	void ShowData() const;
};

// implementation
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) {// 【(int)构造函数】
	++ct;
	cout << "(int) constructor called; number of objects: " << ct << endl;
	pc = new char[n];// 深复制
	ShowObject();
}
Useless::Useless(int k, char ch) : n(k) {// 【(int,char)构造函数】
	++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 constructor 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;// steal address
	f.pc = nullptr;// give old object nothing in return
	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) const {// 【重载加法运算符】
	cout << "Entering operator+()\\n";
	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 {// 【打印成员变量:n和pc地址】
	cout << "Number of elements: " << n;
	cout << ", Data address: " << (void*)pc << endl;
}
void Useless::ShowData() const {// 【打印成员变量pc指针所代表的字符串】
	if (n == 0)
		cout << "(object empty)";
	else
		for (int i = 0; i < n; i++)
			cout << pc[i];
	cout << endl;
}
int main() {
	{
		Useless one(10, 'x');
		Useless two = one;// calls copy constructor
		Useless three(20, 'o');
		Useless four(one + three);// calls operator+()、move constructor AND destructor
		cout << "object one: ";
		one.ShowData();
		cout << "object two: ";
		two.ShowData();
		cout << "object three: ";
		three.ShowData();
		cout << "object four: ";
		four.ShowData();
	}
}

上面程序,每调用任何一种构造函数,即构造对象时,ct++;每调用析构函数,即销毁对象时,ct–。
在VS2019上运行结果如下:

(int, char) constructor called; number of objects: 1
Number of elements: 10, Data address: 007CF2E0
copy constructor called; number of objects: 2
Number of elements: 10, Data address: 007CF468
(int, char) constructor called; number of objects: 3
Number of elements: 20, Data address: 007CD960
Entering operator+()
(int) constructor called; number of objects: 4
Number of elements: 30, Data address: 007CBEE8
temp object:
Leaving operator+()
move constructor called; number of objects: 5
Number of elements: 30, Data address: 007CBEE8
destructor called; objects left: 4
deleted object:
Number of elements: 0, Data address: 00000000
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of elements: 30, Data address: 007CBEE8
destructor called; objects left: 2
deleted object:
Number of elements: 20, Data address: 007CD960
destructor called; objects left: 1
deleted object:
Number of elements: 10, Data address: 007CF468
destructor called; objects left: 0
deleted object:
Number of elements: 10, Data address: 007CF2E0

程序解读:

Useless::Useless(Useless&& f) : n(f.n) {
	++ct;
	cout << "move constructor called; number of objects: " << ct << endl;
	pc = f.pc;// steal address
	f.pc = nullptr;// give old object nothing in return
	f.n = 0;
	// ShowObject();
}
移动构造函数

和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,pc指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了f.pc、f.n,有效避免了“同一块对空间被释放多次”情况的发生。

它让pc指向现有的数据,以获取这些数据的所有权。此时,pc和f.pc指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用delete [ ]两次。为避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete [ ]没有问题。这种夺取所有权的方式常被称为窃取(pilfering)。上述代码还将原始对象的元素数设置为零,这并非必不可少的,但让这个示例的输出更一致。注意,由于修改了f对象,这要求不能在参数声明中使用const。

对于Useless four(one + three); 调用了移动构造函数。

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

注意到对象two是对象one的副本:它们显示的数据输出相同,但显示的数据地址不同(007CF2E0和007CF468)。另一方面,在方法Useless::operator+()中创建的对象的数据地址与对象four存储的数据地址相同(都是007CBEE8),其中对象four是由移动复制构造函数创建的。另外,注意到创建对象four后,为临时对象调用了析构函数。之所以知道这是临时对象,是因为其元素数和数据地址都是0。

移动赋值运算符

适用于构造函数的移动语义考虑也适用于赋值运算符。
下面演示了给Useless类编写复制赋值运算符和移动赋值运算符:

Useless& Useless::operator=(const Useless& f) {// copy assignment
	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) {// move assignment
	if (this == &f)
		return *this;
	delete[] pc;
	n = f.n;
	pc = f.pc;
	f.n = 0;
	f.pc = nullptr;
	return *this;
}

上述移动赋值运算符删除目标对象中的原始数据,并将源对象的所有权转让给目标。
不能让多个指针指向相同的数据,这很重要,因此上述代码将源对象中的指针设置为空指针。
与移动构造函数一样,移动赋值运算符的参数也不能是const引用,因为这个方法修改了源对象。

一般而言,编译器完全可以进行优化,只要结果与未优化时相同。即使您省略该程序中的移动构造函数。
机智的编译器可能自动消除额外的复制工作,但通过使用右值引用,程序员可指出何时该使用移动语义。

以上是关于cpp►C++11右值引用移动语义移动构造函数移动赋值运算符的主要内容,如果未能解决你的问题,请参考以下文章

C++11之右值引用:移动语义和完美转发(带你了解移动构造函数纯右值将亡值右值引用std::moveforward等新概念)

C++11移动构造函数详解

❥关于C++之右值引用&移动语义┇移动构造&移动复制

移动语义的一切

C++11新特性:20—— C++11移动构造函数的功能和用法

右值引用,移动语义,完美转发