c++总结-03-move
01 RAII 习语——资源获取即初始化
• RAII (Resource Acquisition Is Initialization) 是C++内存和资源管理最重要的机制之一。
• RAII通过三个环节来保证内存或资源得到确定性释放
• 1、构造器中获取内存或资源
• 2、析构器中释放内存或资源
• 3、栈对象在作用域结束,即确定性调用析构器、回收内存
• RAII的析构机制是由编译器根据对象生命周期销毁机制自动确保的,无需手工干预,得到确保执行。
• RAII 机制 完善了 C++对值语义的坚持。
• 标准库中RAII无处不在(string,thread,ifstream,unique_ptr……)
02 RAII 的核心优势
• 1. 异常免疫,即使出现异常,也确保执行析构。
• 2. 同时管理内存与非内存资源(如文件句柄、锁、网络IO…)
• 3. 不仅管理栈对象,同时管理堆对象
• 4. 失效即释放,时间确定性(而非主流垃圾收集器的非确定性)
• 5. RAII 针对对象嵌套结构是递归进行的(依赖析构器的递归)。• 6. RAII和移动语义、智能指针结合之后达成了资源管理正确性和性能的双重保障。
• 7. RAII 和 C++对值语义的彻底支持可谓珠联璧合。
• 8. 遵从好的RAII设计规范,是C++实现内存和资源安全的坦途。
03 性能指南——对象与资源管理
• 利用RAII(资源获取即初始化)自动管理资源
• 优先采用有作用域的栈对象,避免不必要的堆分配• 保持作用域尽量小,最小化资源持有时间
• 尽量避免全局变量/静态变量
• 严格避免 malloc() 和 free() (无对象状态管理)• 尽量避免显式调用 new 和 delete
• 同时重载相匹配的分配、回收函数对
04 对象拷贝的代价
• 内含动态内存的对象,值语义下要求执行深拷贝
• 对象拷贝发生的时机有很多,而且有很多隐藏在其他操作之中• 赋值
• 初始化
• 传参
• 返回值
• 交换 swap
• 容器扩容
• …….
• 手工编码避免大对象的拷贝,生命周期管理极易出错,因此
C++11引入了移动操作(move)
05 移动操作的来源
如果被拷贝的对象objA,在拷贝之后确定不再使用,那么直接将目标对象objB的大对象指针指向objA内的大对象指针将大大提升拷贝性能。这就是移动操作的来源。
06 使用对象移动降低集合变更代价
各种集合的push_back, insert, emplace, resize, erase…变更操作
07 移动语义的主要价值
• 移动发挥最大价值的场景:
• 对象内部又有分离内存(通常是指针指向的堆内存)
• 对象拷贝是深拷贝
• 移动仅复制对象内本身的数据,不复制分离内存
• 而拷贝既复制对象内本身的数据,也复制分离内存
• 移动永远不会比拷贝慢,通常更快。
• C++ 通过两个操作来支持移动语义:
• 移动构造函数
• 移动赋值操作符
• 对象可被移动的前提是——移动之后不再使用!此乃右值的来源
08 右值引用
• C++ 11引入右值引用语法:T&&
• 通常的引用T&现在被认为是左值引用
• 右值引用在某些方面和左值引用有类似行为
• 必须被初始化,不能被重新绑定
• 右值引用表示对象可以“从这里移动到别的对象”
#include <iostream>
using namespace std;template<typename T>
void invoke(T& data)
{cout << "T &data" << endl;
}template<typename T>
void invoke(T&& data)
{cout << "T&& data" << endl;
}int main()
{invoke("abc");//左值invoke("abc"s);//stringinvoke(string("abc"));
}
结果:
09 左值与右值
•左值:命名对象、可取地址&,可赋值
•基本类型变量、数组、数组元素
•字符串字面量,如“helloworld”
•对象变量、对象成员
•指针、指针解引用后的对象/变量
•函数(可取地址)
•返回左值的表达式
•右值:无名、无法取地址,不可赋值
•各种临时对象(函数返回值)、字符串临时对象
•除字符串外的其他基本类型字面量
•lambda 表达式
•运算符表达式
10 左值、右值与移动
• 左值(lvalue,left value):有身份、不能移动
• 纯右值(prvalue,“pure” rvalue):没身份、可移动
• 将亡值(xvalue,“eXpiring” value):有身份、可移动
• 当源对象是一个左值,移动左值并不安全,因为左值后续持续存在,可能被引用,虽然可以将左值强制转换为右值,但是要自负安全
• 当源对象是一个右值,移动很安全。
• void func(Widget&& v) 函数形参v,到底是右值、还是左值?
• 对函数调用者来说,v 是个右值引用参数(要传递右值给它)
• 对函数内部来说,v 是个地道的左值引用(可以取地址)
class Point{int m_x;int m_y;
public:Point(int x, int y):m_x(x),m_y(y){}
};class MyClass
{Widget w;int data;};class Widget {Point *data;int value;
public: Widget(int x, int y):data(new Point(x,y)){cout<<"ctor"<<endl;}Widget(const Widget& rhs):value(rhs.value) {if(rhs.data!=nullptr){data=new Point(*rhs.data);}else {data=nullptr;}cout<<"copy ctor"<<endl;} void process(){}Widget(Widget&& rhs): data(rhs.data),// 1. 窃取源对象的指针值value(rhs.value){ rhs.data = nullptr; // 2. 将源对象的值设为有效状态cout<<"move ctor"<<endl; } Widget& operator=(Widget&& rhs) { if(this==&rhs){return *this;}value=rhs.value;delete this->data; // 1. 删除当前值 data = rhs.data; // 2. 窃取源对象的值 rhs.data = nullptr; // 3. 将源对象的值设为有效状态 //cout<<"move assignment"<<endl; return *this; }Widget& operator=(const Widget& rhs) {if(this== &rhs){return *this;}if(rhs.data!=nullptr){if(data!=nullptr){*data=*rhs.data;}else{data=new Point(*rhs.data);}}else{delete data;data=nullptr;}cout<<"copy assignment"<<endl;return *this;} ~Widget(){delete data;cout<<"dtor"<<endl;}
};Widget createWidget()
{Widget w(100,200);//return w;return std::move(w);//不必要!
}void process_c(Widget param)
{}int main()
{Widget w1(10,20);Widget w2 = w1; // 左值源 ⇒ 拷贝构造w1 = w2; // 左值源 ⇒ 拷贝赋值cout<<"=========="<<endl;w1 = std::move(w2);//移动转型 //w2.process(); //危险!cout<<"**********"<<endl;w2 = createWidget(); // 右值源 ⇒ 移动赋值cout<<"----------"<<endl;Widget w3(createWidget()); // 返回值优化 > 移动 > 拷贝// cout<<"&&&&&&&&&&&"<<endl;const Widget w4(10,20);process_c(w4);process_c(std::move(w4)); //常量对象不可移动,退化成拷贝
}
11 实现移动支持
• 移动拷贝构造函数
• 1. 窃取源对象内指针指向的值
• 2. 将源对象内的指针值设为有效状态
• 移动赋值操作符
• 1. 删除当前对象内指针指向的值
• 2. 窃取源对象内指针指向的值
• 3. 将源对象内的指针值设为有效状态
• 类内的对象成员处理、基类对象的处理
• 不要直接拷贝,要使用std::move,从而调用它们的移动操作
• 右值引用参数传递给其他函数被认为是左值,如要移动需要std::move• 除了移动构造和赋值操作,还有
• 赋值型函数(setXXX)也建议支持移动操作
12 绑定规则
• 左值( Lvalues)可以绑定到左值引用( lvalue references )
• 左值不可以绑定到右值引用( rvalue references )
• 右值(Rvalues)可以绑定到左值常量引用( lvalue references to const)• 右值(Rvalues)可以绑定到右值非常量引用( rvalue references to non-const)
13 完美转发
首先简单介绍一下几个概念
(1)直接调用:比如从main()主函数中调用funcLast()函数,这其实就叫做直接调用。
(2)转发:从main()函数中调用funcMiddle()函数,通过funcMiddle()函数调用funcLast()函数,这就叫做转发,funcMiddle()函数被当作一个跳板函数。一般情况下跳板函数都写成一个函数模板。
理解std::forward操作
• 应用于转发引用
• forward只有应用于转发引用,才有意义。
• 有条件的编译时类型转换,没有任何运行时计算
• 当传入的参数是右值,forward将类似std::move函数,转换为右值(注意形参默认是左值),从而保留参数的右值特性。
• 当传入的参数是左值,forward将什么都不做,继续保留参数的左值特性。
• 不要对转发引用,调用std::move,因为可能是左值。
• 如果没有forward,很多函数需要同时提供两种重载(传入左值时,使用左值引用;传入右值时,使用右值引用),代码重复且易错。
template<typename T>
void func(T& param) {cout << "传入的是左值" << endl;
}template<typename T>
void func(T&& param) {cout << "传入的是右值" << endl;
}template<typename T>
void funcMiddle(T&& param) {func(param);
}int main()
{int num = 2021;funcMiddle(num);funcMiddle(2022);system("pause");return 0;
}
结果:
从运行结果可以看出不管传得是左值还是右值最终的调用了左值函数,这不是我们想要的,我们需要传左值则转发最值,传右值则转发右值。那这到底是什么原因呢?
funcMiddle()函数本身的形参是一个万能引用,它即可以接受左值也可以接受右值;第一个funcMiddle()函数调用实参是左值,所以,funcMiddle()函数中调用func()中传入的参数也应该是左值;第二个warp()函数调用实参是右值,根据引用折叠规则,funcMiddle()函数接收的参数类型是右值引用,那么为什么却调用了调用func()的左值版本了呢?这是因为在funcMiddle()函数内部,右值引用类型变为了左值,因为参数有了名称,我们也通过变量名取得变量地址。
那么问题来了,怎么保持函数调用过程中,变量类型的不变呢?这就是我们所谓的“完美转发”技术,在C++11中通过std::forward()函数来实现,这个函数要么返回一个左值,要么返回一个右值,可以说forward是c++ 11标准库提供的专门为转发而存在的函数。我们修改我们的funcMiddle()函数如下:
template<typename T>
void funcMiddle(T&& param) {func(std::forward<T>(param));
}
引用折叠(Universal Collapse)
因为完美转发(Perfect Forwarding)的概念涉及引用折叠。一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:
左值-左值 T& &
# 函数定义的形参类型是左值引用,传入的实参是左值引用
左值-右值 T& &&
# 函数定义的形参类型是左值引用,传入的实参是右值引用
右值-左值 T&& &
# 函数定义的形参类型是右值引用,传入的实参是左值引用
右值-右值 T&& &&
# 函数定义的形参类型是右值引用,传入的实参是右值引用
但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
14 特殊成员函数的五法则
• 回顾三法则:析构函数、拷贝构造函数、赋值操作符 三者自定义其 一,则需要同时定义另外两个(编译器自动生成的一般语义错误)
• 如果没有自定义析构函数、拷贝构造函数、赋值操作符任何其一,编译器也会自动生成移动构造和移动赋值操作符,生成的是按成员进行实例成员的移动操作请求(如果不支持,则退化为拷贝)。
• 如果自定义了析构函数、拷贝构造函数、赋值操作符任何其一 ,那么移动构造和移动赋值操作符,都需要自定义,编译器将不再自动生成(常见陷阱的由来!)
• 如果自定义了移动构造、移动赋值操作符任何其一,编译器将不再自动生成另外一个(注意 和三法则的编译自动生成规则不同)和对应 的拷贝构造、或赋值操作符。
• 简单规则:五大特殊成员函数要么全部自定义(指针指向动态数据成员),要么全交给编译器自动生成(基本类型或对象成员)。