在对象拷贝后就立即被销毁的情况下,移动而非拷贝对象会大幅度提升性能。以及像 IO 类或 unique_ptr 这样的类,包含不能被共享的资源,不能拷贝但可以移动。而标准库容器、string 和 shared_ptr 类既支持移动也支持拷贝
右值引用:
右值引用是 c++11 为了支持移动操作引入的。右值引用就是只能绑定到右值的引用。我们通过 && 而非 & 来获取右值引用。右值引用只能绑定到一个将要销毁的对象。因此我们可以自由地将一个右值引用的资源 “移动” 到另一个对象中
类似任何引用,右值引用也是某个对象的别名。但右值引用有着与左值引用有着完全相反的引用特性:我们可以将一个右值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式,但不能将一个右值引用绑定到一个左值上:
int main(){
int i=42;
int &r=i;
// const int &&rr=i;//错误,只能绑定右值
// int &r2=i*42;//错误,只能绑定左值
const int &r2=i*42;//常量左值引用可以绑定左值,
int &&rr2=i*42;//定义一个右值引用,那么这个临时变量的生命周期将会和rr2一样长
//int &&rr3=rr2;//错误:rr2是一个左值
int &&rr3=42;//错误,rr3是一个左值
int &&rr4=float(42);//转换表达式会生成一个右值
rr4=rr3;//右值引用可以赋值右值引用
return 0;
}
注意:返回左值引用的函数,连同赋值、下标、解引用和前置递增 / 递减运算符都是返回左值的。而返回非引用类型的函数,连同算术、关系、位以及后置递增 / 递减运算符都生成右值
变量都是左值,右值引用类型的变量同样是左值,因此我们不能将一个右值引用绑定上一个右值引用类型的变量上
类似于左值引用,右值引用引用之间也可以赋值
左值持久,右值短暂:
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
由于右值引用只能绑定到临时对象,我们得知:
1、所引用的对象将要被销毁
2、该对象没有其它用户
这意味着:使用右值引用的代码可以自由的接管所引用的对象的资源
标准库 move 函数:
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为 move 的库函数来获得绑定到左值上的右值引用,此函数定义在头文件 utility 中:
#include<bits/stdc++.h>
using namespace std;
int main(){
int cnt=1;
int &&gg=std::move(cnt);
return 0;
}
move 调用告诉编译器,我们有一个左值但我们希望像处理右值一样处理它。
调用 move 也意味着承若:除了对 cnt 赋值或销毁外,我们将不再使用它。在调用 move 之后,我们不能对移后源对象的值做任何假设
移动构造函数和移动赋值运算符:
移动构造函数的第一个参数必须是右值引用,其余参数都必须有默认实参
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些源对象的所有权已经归属新创建的对象:
StrVec::StrVec(StrVec &&s) noexcept :
elements(s.elements), first_free(s.first_free), cap(s.cap) {
//令s进入这样的状态——对其运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
注意:noexcept 通知标准库我们的构造函数不抛出任何异常
与拷贝构造函数不同,移动构造函数不分配任何新内存,它接管给定的 StrVec 中的内存。在接管内存之后,将给定对象中的指针都置为 nullptr,保证完成移动构造函数后源对象被销毁不会释放掉我们刚刚移动的内存
移动操作、标准库容器和异常:
移动构造函数通常不分配任何资源,因此移动构造操作通常不会抛出任何异常。我们应该将此事通知标准库,否则标准库会认为移动我们的类对象可能会抛出异常并为了处理这种可能性而做一些额外的工作
一种通知标准库的方法是在我们的移动构造函数中指明 noexcept。在一个构造函数中,noexcept 出现在参数列表和初始化列表的冒号之间:
#include<bits/stdc++.h>
using namespace std;
class Node{
Node(Node&&)noexcept;
};
Node::Node(Node &&x)noexcept:/*成员初始化器*/{
}
注意:不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept
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;//将rhs置于可析构状态
}
return *this;
}
注意:特殊处理自赋值的情况
不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept
在移动操作后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设
合成的移动操作:
如果我们不声明自己的拷贝构造函数或拷贝赋值运算符,编译器总会为我们合成这些操作。
与拷贝操作不同,特别是.如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。
如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作:
#include<bits/stdc++.h>
using namespace std;
class X{
int i;//内置类型可以移动
string s;//string 有自己的移动函数
};
class hasx{X x;}; //有合成的移动操作
int main(){
X x,x1=std::move(x);//使用合成的移动函数
hasx y,y1=std::move(y);//使用合成的移动函数
return 0;
}
注意:只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非 static 数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
移动操作的删除函数:
与拷贝操作不同,移动操作永远不会隐式定义为删除函数。但是,如果我们显示地要求编译器生成 = default 的移动操作,且编译器不能移动所有非 static 成员,则编译器会将移动操作定义为删除函数。一般情况下,合成的移动操作定义为删除的函数要遵循与定义为删除的合成拷贝操作类似的原则:
1、与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值的情况类似
2、如果有类所有的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的
3、类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
4、类似拷贝赋值运算符,如果有类成员是 const 的或是引用的,则类的移动赋值运算符被定义为删除的
#include<bits/stdc++.h>
using namespace std;
class Y{
public :
Y()=default;
Y(Y&&)=delete;//定义删除的
};
class hasx{//移动构造函数被定义为删除的
public :
hasx()=default;
private:
Y mm;
};
int main(){
hasx h1,h2=std::move(h1);//错误,移动构造函数被定义为删除的
return 0;
}
注意:定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的
移动右值,拷贝左值:
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数:
#include <iostream>
#include "StrVec.h"
using namespace std;
StrVec getVec(void) {
StrVec gg;
return gg;
}
int main(void) {
StrVec v1, v2;
v1 = v2;//v2是左值,使用拷贝赋值
v2 = getVec();//getVec()返回一个右值,使用移动赋值
return 0;
}
如果没有移动构造函数,右值也被拷贝:
#include<bits/stdc++.h>
using namespace std;
class Node{
public :
Node()=default;
Node(const Node &a):x(a.x){
cout<<"拷贝构造函数"<<endl;
}
private:
int x;
};
int main(){
Node a;
Node b(a);//拷贝赋值
Node c(std::move(a));//没有定义移动构造函数,拷贝构造函数,因为参数是const 的引用,所以可以指向左值
}
移动迭代器:
c++11 标准库中定义了一种移动迭代器适配器,移动迭代器解引用生成一个右值引用。我们通过调用 make_move_iterator 函数将一个普通迭代器转换成一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。原迭代器的所有操作在移动迭代器中都照常工作:
#include<bits/stdc++.h>
using namespace std;
int main(){
allocator<int> alloc;
int tt[100];
auto now=alloc.allocate(100);//开辟100个空间
auto las = uninitialized_copy(make_move_iterator(begin(tt)),make_move_iterator(end(tt)),now);//在里面移动tt里面的元素。
return 0;
}
注意:标准库不保证哪些算法适用移动迭代器,哪些不适用。由于移动一个对象可能销毁原对象,因此只有确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能使用移动迭代器传递给算法
右值和左值引用成员函数:
通常,我们在一个对象上调用成员函数,而不管对象是一个左值还是一个右值.
#include<bits/stdc++.h>
using namespace std;
int main(){
string s1="value1",s2="value2";
string s3=(s1+s2).substr(1,3);
cout<<s3<<endl;
s1+s2="这样虽然没有意义,但是是合法的";
return 0;
}
引用限定符号:
&&右值限定,&左值限定。
#include <iostream>
using namespace std;
class Foo{
public:
Foo& operator=(const Foo&) &;//只能向可修改的左值赋值,
// &和const一样用来修饰this指向的对象的
// Foo();
// ~Foo();
// Foo someMem() & const;//错误,const 限定符必须在前
Foo anotherMem() const &;//正确,const 限定符在前
Foo gg() const &&;//this 也可以指向一个右值
// Foo yy() && const;//指向右值时const同样不能在后面
};
Foo& Foo::operator=(const Foo &rhs) & {//引用限定符和const一样必须同时出现在声明和定义中
//执行将rhs赋予本对象所需的工作
return *this;
}
Foo& retFoo(){//返回一个左值
return *(new Foo());
}
Foo retVal(){//返回一个右值
return Foo();
}
int main(void){
Foo i, j;//i和j是左值
i = j;//正确,i是左值
retFoo() = j;//正确,retFoo() 返回一个左值
// retVal() = j;//错误,retVal() 返回一个右值
i = retVal();//正确,我们可以将一个右值作为赋值运算符右侧运算对象
return 0;
}
注意:引用限定符可以是 & 或 &&,分别指出 this 可以指向一个左值或右值。
类似 const 限定符,引用限定符只能用于非 static 成员函数且必须同时出现在函数的声明和定义中
一个函数可以同时用 const 和引用限定。但引用限定符必须跟随在 const 限定符之后