右值引用
右值引用
\(所谓右值引用就是必须绑定到右值的引用.\\符号是\&\&.只能绑定到一个将要销毁的对象.\\因此.我们可以自由地将一个右值引用的资源"移动"到另一个对象.\)
\(右值和左值都是表达式的属性.\\一般而言,一个左值表达的是一个对象的身份,而右值表达式表达是一个对象的值.\)
\(类似任何引用,一个右值引用不过是某个对象的另一个名字而已.\\对左值来说,我们不能将其绑定要求\color{red}{转换的表达式,字面常量或是返回右值的表达式}.\\右值则完全相反,右值可以绑定这类表达式上,但右值不能绑定到一个左值上.\)
int i = 42
int &r = i; // 正确: r引用i
int &&rr = i; // 错误: 不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误: i*42是一个右值
const int &r3 = i * 42; // 正确: 我们可以将一个const的左值引用绑定到一个右值上
int &&rr2 = i * 42 // 正确: 将rr2绑定到乘法结果上
\(返回左值表达式的例子:返回左值引用的函数,赋值,下标,解引用和前置递增(递减)运算符.\)
\(返回右值表达式的例子:返回非引用类型的函数,连同算术,关系,位以及后置递增(递减)运算符.\)
左值持久, 右值短暂
\(左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象.\)
\(由于右值引用只能绑定到临时对象,我们得知:\)
- \(所引用的对象即将销毁\)
- \(该对象没有其他用户\)
\(这两个特性意味着:使用右值引用的代码可以自由地接管所引用地对象的资源.\)
\(右值引用指向将要销毁的对象.因此,我们可以从绑定到右值引用的对象"窃取"状态.\)
\(我们不能将一个右值引用绑定到一个右值引用类型的变量上:\)
int &&rr1 = 42; // 正确: 字面常量是右值
int &&rr2 = rr1; // 错误: 表达式rr1是左值
\(右值是表达临时对象的,显然,rr1变量是持久的,直到离开作用域才销毁,那么右值不能绑定它.\)
move函数
\(虽然不能将一个右值直接绑定到左值上,但我们可以显示地将一个左值转换成对应的右值引用类型.\\move函数可以获得绑定在左值上的右值引用.\)
int &&rr3 = std::move(rr1);
\(move调用告诉编译器:我们有一个左值,我们需要它的右值.\\调用move意味着承诺:除了对rr1赋值或销毁外,我们将不再使用它.\\调用move后,我们不能对移后源对象的值做任何假设.\)
\(我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值.\)
移动构造函数
\(为了让自已的类支持移动操作,需要为其定义移动构造函数和移动赋值函数.\)
\(这俩个成员类似对应的拷贝拷贝操作,但它们会从给定对象"窃取"资源而不是拷贝资源.\)
\(移动拷贝函数的第一参数是该类型的一个引用.与拷贝参数一样,任何额外的参数都必须要有默认实参.\)
\(除了完成资源移动,移动构造函数还必须确保移后源对象销毁后是无害的.\\一旦资源完成移动,源对象必须不再指向被移动的对象:这些资源的所有权已经归属于新创建的对象.\)
\(例子:\)
StrVec::StrVec(StrVec &&s) noexcept // 移动操作不会抛出异常
// 成员初始化器接管s的资源
: elements(s.elements), first_free(s.first_free), cap(s.cap) {
s.elements = s.first_free = s.cap = nullptr; // 使s运行析构函数安全
}
\(与拷贝构造不同,移动构造不分配任何内存:它接管给定StrVec的内存.\\在接管内存后,它将给定对象中的指针都置为nullptr,此对象仍存在.最终,移后源对象会被销毁,意味在其上运行析构函数.\\StrVec的析构函数在first\_free上调用deallocate.如果我们忘记改变first\_free,则销毁移后源对象就会释放我们刚刚移动的内存.\)
移动赋值函数
\(移动赋值函数执行与析构函数和移动构造函数与析构函数相同的工作.\\类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:\)
StrVec &StrVec::operator=(StrVec &&rhs) noexcept {
if (this != &rhs) {
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
\(检测自赋值是因为此右值可能是move调用的返回结果.\)
移后源对象必须可析构
\(从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁.\\因此必须确保移后源对象可以进入一个可析构的状态.\)
\(除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效的.\\一般来说,对象有效就是指可以安全地为其赋新值或者可以安全地使用而不依赖其当前值.\\移动操作对移后源对象中留下的值没有任何要求.\)
\(例如,对于一个string移动数据后,我们知道移后源对象仍有效,我们可以对它进行如empty或size等操作.\\但是,调用的结果是未定义的.\)
\(总结:移动操作后,移后源对象必须保证有效的,可析构的,但是我们不能对其值作出任何假设.\)
合成的移动操作
\(只有当一个类没有定义任何自已版本的拷贝控制成员,且类的每个非staic成员数据都可以移动时,\\编译器才hi为它合成移动构造函数或移动赋值函数.编译器可以移动内置成员,如果一个成员类有自已的移动操作,\\编译器也可以移动这个类.\)
struct X {
int i; // 内置类型可以移动
std::string s; // string定义了自已的移动操作
};
struct hasX {
X men; // X有合成的移动操作
};
// 使用合成的移动构造
X x, x2 = std::move(x);
hasX hx, hx2 = std::move(hx);
\(与拷贝操作不同,移动操作永远不会隐式定义为删除的函数.但是,如果我们显示地要求生成=default的\\移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数.\)
\(例如:假定Y是一个类,它定义了自已的拷贝构造函数但未定义自已的移动构造函数.\)
struct hasY {
hasY() = default;
hasY(hasY&&) = default;
Y mem; // hasY将有一个删除的移动构造函数
};
hasY hy, hy2 = std::move(hy); // 错误: 移动构造函数时删除的
\(移动操作和合成的拷贝控制成员间的相互关系:如果类定义了一个移动构造函数和/或一个移动赋值函数运算符,\\该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的.反之亦然.\)
移动右值, 拷贝左值
\(如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通函数匹配规则来确认使用哪个.\\在StrVec类中,拷贝构造函数接受一个const\ StrVec的引用.因此它可以用于任何可以转换为StrVec的类型,\\而移动构造函数接受一个StrVec\&\&,因此只能用于实参(非static)右值的情形.\)
StrVec v1, v2;
v1 = v2; // v2是左值,使用拷贝构造
StrVec getVec(istream &); // getVec返回一个右值
v2 = getVec(cin); // getVec(cin)是一个右值,使用移动赋值
如果没有移动构造函数,右值也将被拷贝
\(在有拷贝构造无移动构造时,编译器不会合成移动构造函数.\)
\(此情况下,函数匹配规则表示该类型的对象会被拷贝,即使使用move函数也一样:\)
class FOO {
public:
Foo() = default;
Foo(const Foo&); // 拷贝构造
};
Foo x;
Foo y(x); // 拷贝构造, x是左值
Foo z(std::move(x)); // 还是拷贝构造,因为无移动构造
\(用拷贝构造代替移动构造肯定是安全的.\)
拷贝并交换赋值运算符和移动操作
\(我们可以让赋值运算符既是移动赋值运算符,也是拷贝运算符:\)
class HasPtr {
public:
HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i), {p.ps = 0;}
HasPtr& operator=(HasPtr rhs) {
swap(*this, rhs);
return *this;
}
// 定义其他成员
};
\(对赋值运算符,它有一个非引用参数,意味着此参数要拷贝初始化.依赖于实参的类型,\\拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数:左值被拷贝,右值被移动.\)
hp = hp2; // hp2通过拷贝构造来拷贝
hp = std::move(hp2); // 移动构造函数移动hp2
引用限定符
\(通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值.\)
string s1 = "abc", s2 = "123";
auto n = (s1 + s2).find('a');
\(在此例中,我们在一个右值上调用了函数成员.有时,右值的使用方式令人惊讶:\)
s1 + s2 = "wow!";
\(我们对一个右值赋值.\)
\(很多时候,我们需要阻止这种使用方式,我们希望强制左侧运算对象(即this所指对象)是一个左值.\)
\(指出this的左值/右值属性是在参数列表后加引用限定符.\)
class Foo {
public:
Foo &operator=(const Foo &) &; // 只能向可修改的左值赋值
};
Foo &Foo::operator=(const Foo &rhs) & {
..... // 执行rhs赋给本对象的工作
return *this;
}
\(引用限定符可以是\&或\&\&,分别指出this可以是一个左值或右值.类似const限定符,引用限定符\\只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中.\)
\(一个函数可以同时用const和引用限定.在此情况下引用限定符必须跟在const后面.\)
Foo anotherMen() const &;
重载和引用函数
\(const和引用限定符都可以区分重载.\)
class Foo {
public:
Foo sorted() &&;
Foo sorted() const &;
private:
vector<int> date;
};
Foo Foo::sorted() && { // 本对象为右值,可以原址排序
sort(date.begin(), date.end());
return *this;
}
Foo Foo::sorted() const & { // 本对象是const或是一个左值,哪种情况我们都不能对其进行原址排序
Foo ret(*this);
sort(ret.date.begin(), ret.date.end());
return this;
}
\(对象是一个右值,意味着没有其他用户,因此我们可以改变对象.\\当对一个const右值或是一个左值执行sorted时,我们不能改变对象,因此需要排序前拷贝.\)
retVal().sorted(); // retVal()是一个右值,调用Foo::sorted() &&
retFoo().sorted(); // retFoo()是一个左值,调用Foo::sorted() const &
\(定义const成员函数,可定义两个版本,唯一差别在于有无const.\\而引用限定不一样,如果定义两个或两个以上有相同名称和形参列表的函数成员,\\要么都加引用限定符,要么都不加.\)
class Foo() {
public:
Foo sorted() &&;
Foo sorted() const; // 错误:必须加上引用限定符
}