[C++ Primer]对象移动

13.6.1 右值引用

右值引用 - 必须绑定到右值的引用
通过 & 获得左值引用(常规引用),通过 && 获得右值引用

int i = 42;
int &r = i;				// 正确:r 引用 i
int &&rr = i;			// 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42;		// 错误:不能将一个左值引用绑定到一个右值上
int const&r3 = i * 42;	// 正确:可以将一个 const 引用绑定到一个右值上
int &&rr2 = i * 42;		// 正确:rr2 引用 乘法结果

(1) 左值持久,右值短暂

左值有持久的状态,而右值要么是字面常量,要么是表达式求值过程中创建的临时变量

字面常量:‘a’,42,“Hello,World!”
临时变量:5 * 36

由于右值引用只能绑定到临时对象 - 右值,知右值引用的两个特性:

右值引用的对象将要被销毁
该对象没有其他用户

这两个特性意味着:使用右值引用的代码可以自由接管所引用的对象的资源

(2) 变量是左值

变量是左值,不能绑定到右值引用上,所以我们很少犯如下错误:

int i = 42;
int &&r = i;

但是注意:不能将一个右值引用绑定到另一个右值引用类型的变量上,如下所示:

int &&rr1 = 42;
int &&rr2 = rr1;	// 错误:右值引用变量 rr1 是左值
int &r1 = i;
int &r2 = r1;		// 正确:引用的引用

(3) 标准库 move 函数

虽然不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型
通过 move 函数来获得绑定到左值上的右值引用,此函数定义在头文件 utility

int &&rr1 = 42;
int &&rr2 = std::move(rr1);

move 调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它
调用 move 意味着承诺:除了对左值(rr1)赋值或销毁它外,我们将不再使用它的值(特别针对类类型)

13.6.2 移动构造函数和移动赋值运算符

(1) 移动构造函数

移动构造函数第一个参数是一个右值引用,任何额外参数必须有默认实参
移动构造函数必须确保移后源对象处于这样一个状态:销毁它是无害的
注意:一旦资源完成移动,源对象必须不再指向被移动的资源,这些资源的所有权已经归属新创建的对象
为 my_class 类定义移动构造函数,如下所示:

class my_class {
	_Ty* resource;
public:
	my_class() = default;
	my_class(my_class&&)noexcept;	// 移动操作不应抛出任何异常
	~my_class();
};

my_class::~my_class() {
	if (resource)
		delete resource;
}

my_class::my_class(my_class&& s)noexcept
	// 接管 s 中的资源
	:resource(s.resource) {
	// 令 s 进入这样的状态:对其运行析构函数是安全的
	s.resource = nullptr;
}

注意:不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept
标准库默认认为移动构造函数在移动类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作

(2) 移动赋值运算符

注意:检测自赋值

my_class& my_class::operator=(my_class&& s)noexcept{
	// 直接检测自赋值
	if (this != &s) {
		if (resource)
			delete resource;	// 释放已有资源
		resource = s.resource;	// 从 s 接管资源
		if (s.resource)
			delete s.resource;	// 将 s 置于可析构状态
	}
	return *this;
}

进行自赋值检查的原因:
右值可能是 move 调用的结果,不能在使用右侧运算对象资源之前就释放左侧运算对象资源(可能是相同的资源)

(3) 移后源对象必须可析构

在移动操作之后,移后源对象必须保持有效的可析构的状态,但是用户不能对其值有任何假设
有效的:可以安全地为其赋新值或者可以安全地使用而不依赖其当前值
不能对其值有任何假设:当我们从标准库 string 移动数据时,我们可能期待一个移后源对象是空的,但是对它执行 emptysize 这些操作的结果并没有保证
(我们的程序不应该依赖于移后源对象中的数据)

(4) 合成的移动操作

如果不声明自定义的拷贝构造函数或拷贝赋值运算符,编译器总会自动合成这些操作
与拷贝操作不同,编译器不会为某些类合成移动操作:

  1. 编译器不会为定义了拷贝构造函数、拷贝赋值运算符和析构函数的类合成移动操作
  2. 当一个类的某些数据成员不能移动构造或移动赋值时,编译器不会为它合成移动操作

如果一个类没有移动操作,通过正常的函数匹配,类会使用拷贝操作来代替移动操作

struct X {
	int i;			// 内置类型可以移动
	std::string s;	// string 定义了自己的移动操作
};					// 编译器会为 X 合成移动操作

struct hasX {
	X mem;			// X 有合成的移动操作
};					// 编译器会为 hasX 合成移动操作

// 使用合成的构造函数
X x; hasX hx;
X x2 = std::move(x);
hasX hx2 = std::move(hx);

编译器合成移动操作的条件

一个类没有定义移动操作
一个类没有定义任何自己版本的拷贝控制成员
一个类的所有数据成员都能移动构造或移动赋值(无论这些移动操作是自定义的还是合成的)
一个类没有定义析构函数

例:定义析构函数阻止合成移动操作

struct my_class {
	my_class() = default;
	my_class(my_class const&) {
		cout << "copy-constructor" << endl;
	}
	~my_class() {}
};

int main() {
	my_class x;
	my_class x1 = std::move(x);
}

运行结果:

copy-constructor

定义析构函数阻止了合成移动操作,类使用拷贝操作来代替移动操作
与拷贝操作不同,移动构造函数永远不会隐式定义为删除的函数
如果显式地要求编译器生成 =default 的移动操作且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数
=default 将移动操作定义为删除的函数的例子:

有类成员定义了拷贝构造函数且未定义移动构造函数
有类成员未定义拷贝构造函数且编译器不能为其合成移动操作
有类成员的移动操作被定义为删除的或不可访问的
有类成员是 const 的或是引用

例:Y 是一个类,它定义了拷贝构造函数但未定义移动构造函数

struct hasY {
	Y mem;
	hasY() = default;
	hasY(hasY&&) = default;	// =default 将 hasY 的移动构造函数定义为删除的函数
};

hasY hy;
hasY hy2 = std::move(hy);	// 移动构造函数是删除的,使用拷贝操作来代替移动操作

注意:定义了拷贝操作的类不会合成移动操作,定义了移动操作的类对拷贝操作的合成也有影响
定义了移动操作的类,合成的拷贝操作会被定义为删除的
综上:定义拷贝影响合成移动,定义移动影响合成拷贝,同时定义拷贝和移动以确保类对象正常工作

(5) 移动右值,拷贝左值

如果一个类既有移动构造函数,又有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数

struct my_class {
	my_class() = default;
	my_class& operator=(my_class const&) {
		cout << "copy-constructor" << endl;
		return *this;
	}
	my_class& operator=(my_class&&) {
		cout << "move-constructor" << endl;
		return *this;
	}
};

my_class create() {
	return my_class();
}

int main() {
	my_class mc1, mc2;
	mc1 = mc2;			// mc2 是左值,使用拷贝构造
	mc2 = create();		// create() 是右值,使用移动构造
}

运行结果:

copy-constructor
move-constructor

如果没有移动构造函数,右值也被拷贝

struct Foo {
	Foo() = default;
	Foo(Foo const&) {
		cout << "copy-constructor" << endl;
	}
};

int main() {
	Foo x;
	Foo y(x);				// x 是左值,使用拷贝构造
	Foo z(std::move(x));	// 未定义移动构造函数,拷贝右值 std::move(x)
}

运行结果:

copy-constructor
copy-constructor

(6) 三五法则

三:拷贝构造函数、拷贝赋值运算符、析构函数
五:拷贝构造函数、拷贝赋值运算符、析构函数、移动构造函数、移动赋值运算符
如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作

(7) 移动迭代器(move iterator)

移动迭代器的解引用运算符生成一个右值引用
通过调用标准库函数 make_move_iterator 将一个普通迭代器转换为一个移动迭代器
注意:标准库不保证哪些算法使用移动迭代器,哪些不适用
建议:在移动构造函数和移动赋值运算符的实现代码之外的地方,不要随意使用移动操作

13.6.3 右值引用和成员函数

除了构造函数和赋值运算符之外,一个成员函数同时提供拷贝和移动版本也有好处
这种函数通常一个版本接受指向 const 的左值引用,另一个版本接受指向非 const 的右值引用

(1) 引用限定符

在 C++ 旧标准中,没有办法阻止对右值进行赋值,如下所示:

string s1, s2;
s1 + s2 = "wow!";

在此情况下,我们希望阻止这种用法,即希望强制左侧运算对象是一个左值
在参数列表后放置一个引用限定符,强制 this 指向对象的左值/右值属性

class my_string {
	char* data;
	size_t size, capacity;
public:
	// 只可用于非 const 的左值
	my_string& operator=(my_string const& _Right)&;
};

引用限定符可以是 &&&,分别指出 this 指向一个左值或右值:

// 可用于非 const 的右值
my_string& my_string::operator=(my_string const& _Right)&&;

成员函数可以同时用 const 和引用限定,但引用限定符必须在 const 限定符之后

// 可用于任何类型的 my_string
my_string& my_string::operator=(my_string const& _Right)const&; 

注意:引用限定符对于构造函数/析构函数非法

(2) 重载和引用函数

引用限定符可以区分成员函数的重载版本,如下所示:

class my_vector {
	int* data;
	size_t size, capacity;
public:
	my_vector sorted()&&;		// 可用于非 const 的右值
	my_vector sorted()const&;	// 可用于任何类型的 my_vector
};
// this 指向右值,没有其他用户,可以原址排序
my_vector my_vector::sorted()&& {
	sort(data, data + size);
	return *this;
}
// this 指向 const 对象或左值,不能原址排序
my_vector my_vector::sorted()const& {
	my_vector ret(*this);
	sort(ret.data, ret.data + size);
	return ret;
}

my_vector().sorted();	// 精确匹配参数为非 const 的右值的重载版本
my_vector nums;
nums.sorted();			// 匹配参数为 const& 的泛用重载版本

注意:如果一个成员函数有引用限定符,则具有相同参数列表的所有重载版本都必须有引用限定符

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值