什么是移动语义?
移动语义是 C++11 引入的核心特性,它的核心思想是「转移资源所有权,而非深拷贝资源」—— 用极低的成本(仅交换几个指针 / 整数)把一个对象的堆资源 “偷” 给另一个对象,同时让源对象处于 “有效但可析构的空状态”,从而彻底避免不必要的深拷贝,大幅提升程序性能。移动语义使得在 C++ 里返回大对象(如容器)的函数和运算符成为现实,因而可以提高代码的简洁性和可读性,提高程序员的生产率。
值类型
C++11 之后,值类别(Value Categories) 是一套严格的标准分类,不是简单的「左 = 左边、右 = 右边」,而是分为 3 个基本类型 + 2 个组合类型,这是理解移动语义、引用绑定、智能指针的底层根基。
左值(lvalue)
有名字、可以取地址、生命周期持久的表达式。它是 “活着的、稳定的对象”。
特征:可以放在=左边;可以用&取地址;不能直接被移动(除非用 std::move 强行转);能绑定到 左值引用 T&。
纯右值(prvalue)
无名字、不能取地址、临时的字面量 / 临时值。就是 C++11 之前传统意义上的「右值」。
特征:不能放在 = 左边;不能用 & 取地址;天生可以被移动;能绑定到 右值引用 T&& / const T&。
将亡值(xvalue)
C++11 新增有名字、能取地址、但即将被销毁、可以被安全移动的表达式。它是「左值的身体,右值的灵魂」。
特征:有身份(glvalue);可以被移动(rvalue);是移动语义的核心来源;能绑定到 右值引用 T&&。
对象的生命周期
C++对象生命周期的本质是一个对象从 诞生(构造) 到 死亡(析构) 的这段时间,就是它的生命周期。
栈对象(局部变量): 生命周期 从进入作用域创建到离开作用域销毁;析构顺序按照构造的逆序(后构造先析构)。
堆对象(new/malloc): 生命周期 从new开始,到delete结束;不进行delete对导致 内存泄漏,与作用域无关。
静态/全局对象: 生命周期 从程序开始到程序结束;在第一次进入作用域时构造,在main结束后逆序析构。
临时对象的生命周期
1、默认生命周期: 临时对象在「完整表达式结束」(即分号;处)死亡。
2、函数调用中的临时对象顺序
foo( A(), B() );
这行代码的执行顺序为:
-
计算参数 A ()、B (),构造参数临时对象
-
执行函数 foo
-
函数返回,构造返回值临时对象
-
返回值临时对象 先使用、先析构
-
参数临时对象 后析构(逆序构造顺序)
-
完整表达式结束
临时对象生命周期延长规则
规则:当一个临时对象被const T&和T&&这两种引用直接绑定时,它的生命周期会被延长到引用本身的生命周期。
即C&& c = foo( A(), B());,这时函数返回的临时对象的生命周期会跟随c至main结束。
但是生命周期的延长也存在限制:只有“直接绑定”才能延长;函数返回的引用不能延长生命周期(const C& f() { return C; }返回的临时对象在函数结束就死了,外面拿到悬空引用);std::move 本身不改变生命周期,只改变类型标签。
实现移动
要让设计的对象支持移动,通常需要下面几步:
- 对象应该有分开的拷贝构造和移动构造函数(除非只打算支持移动,不支持拷贝——如
unique_ptr)。 - 对象应该有
swap成员函数,支持和另外一个对象快速交换成员。 - 在对象的名空间下,应当有一个全局的
swap函数,调用成员函数swap来实现交换。支持这种用法会方便在其他对象里包含你的对象,并快速实现它们的swap函数。 - 实现通用的
operator=。 - 上面各个函数如果不抛异常的话,应当标为
noexcept。这对移动构造函数尤为重要。
引用坍塌
在 C++ 中,不允许 “引用的引用”,但模板 /typedef/auto 里会出现 T&& && 这种叠加,编译器必须把它化简成一个引用 —— 这个化简规则就叫「引用坍塌」。C++ 语法明文规定: 不能直接写 int& & 或 int& &&,编译报错。 但在模板推导 、typedef、decltype 中,会间接产生这种叠加,编译器必须偷偷处理掉。
引用坍塌的4条规则:只要有一个 &,结果就是 &;两个 && 才是 &&
T& &→T&T& &&→T&T&& &→T&T&& &&→T&&
万能引用 T&&:
template <typename T>
void f(T&& t);
- 传左值
int&→T&& = int& && → int& - 传右值
int→T&& = int&&
这就是为什么万能引用既能接收左值,又能接收右值。