移动语义和值类别

0 阅读4分钟

什么是移动语义?

移动语义是 C++11 引入的核心特性,它的核心思想是「转移资源所有权,而非深拷贝资源」—— 用极低的成本(仅交换几个指针 / 整数)把一个对象的堆资源 “偷” 给另一个对象,同时让源对象处于 “有效但可析构的空状态”,从而彻底避免不必要的深拷贝,大幅提升程序性能。移动语义使得在 C++ 里返回大对象(如容器)的函数和运算符成为现实,因而可以提高代码的简洁性和可读性,提高程序员的生产率。

值类型

C++11 之后,值类别(Value Categories) 是一套严格的标准分类,不是简单的「左 = 左边、右 = 右边」,而是分为 3 个基本类型 + 2 个组合类型,这是理解移动语义、引用绑定、智能指针的底层根基

image.png

左值(lvalue)

有名字、可以取地址、生命周期持久的表达式。它是 “活着的、稳定的对象”。

特征:可以放在=左边;可以用&取地址;不能直接被移动(除非用 std::move 强行转);能绑定到 左值引用 T&

纯右值(prvalue)

无名字、不能取地址、临时的字面量 / 临时值。就是 C++11 之前传统意义上的「右值」。

特征:不能放在 = 左边;不能用 & 取地址;天生可以被移动;能绑定到 右值引用 T&& / const T&

将亡值(xvalue)

C++11 新增有名字、能取地址、但即将被销毁、可以被安全移动的表达式。它是「左值的身体,右值的灵魂」。

特征:有身份(glvalue);可以被移动(rvalue);是移动语义的核心来源;能绑定到 右值引用 T&&

image.png

对象的生命周期

C++对象生命周期的本质是一个对象从 诞生(构造)死亡(析构) 的这段时间,就是它的生命周期。

栈对象(局部变量): 生命周期 从进入作用域创建到离开作用域销毁;析构顺序按照构造的逆序(后构造先析构)。

堆对象(new/malloc): 生命周期 从new开始,到delete结束;不进行delete对导致 内存泄漏,与作用域无关。

静态/全局对象: 生命周期 从程序开始到程序结束;在第一次进入作用域时构造,在main结束后逆序析构。

临时对象的生命周期

1、默认生命周期: 临时对象在「完整表达式结束」(即分号;处)死亡。

2、函数调用中的临时对象顺序

foo( A(), B() );

这行代码的执行顺序为:

  • 计算参数 A ()、B (),构造参数临时对象

  • 执行函数 foo

  • 函数返回,构造返回值临时对象

  • 返回值临时对象 先使用、先析构

  • 参数临时对象 后析构(逆序构造顺序)

  • 完整表达式结束

临时对象生命周期延长规则

规则:当一个临时对象被const T&T&&这两种引用直接绑定时,它的生命周期会被延长到引用本身的生命周期。 即C&& c = foo( A(), B());,这时函数返回的临时对象的生命周期会跟随cmain结束。

但是生命周期的延长也存在限制:只有“直接绑定”才能延长;函数返回的引用不能延长生命周期(const C& f() { return C; }返回的临时对象在函数结束就死了,外面拿到悬空引用);std::move 本身不改变生命周期,只改变类型标签。

实现移动

要让设计的对象支持移动,通常需要下面几步:

  • 对象应该有分开的拷贝构造和移动构造函数(除非只打算支持移动,不支持拷贝——如 unique_ptr)。
  • 对象应该有 swap 成员函数,支持和另外一个对象快速交换成员。
  • 在对象的名空间下,应当有一个全局的 swap 函数,调用成员函数 swap 来实现交换。支持这种用法会方便在其他对象里包含你的对象,并快速实现它们的 swap 函数。
  • 实现通用的 operator=
  • 上面各个函数如果不抛异常的话,应当标为 noexcept。这对移动构造函数尤为重要。

引用坍塌

在 C++ 中,不允许 “引用的引用”,但模板 /typedef/auto 里会出现 T&& && 这种叠加,编译器必须把它化简成一个引用 —— 这个化简规则就叫「引用坍塌」。C++ 语法明文规定: 不能直接写 int& &int& &&,编译报错。 但在模板推导typedefdecltype 中,会间接产生这种叠加,编译器必须偷偷处理掉。

引用坍塌的4条规则:只要有一个 &,结果就是 &;两个 && 才是 &&

  1. T& &T&
  2. T& &&T&
  3. T&& &T&
  4. T&& &&T&&

万能引用 T&&:

template <typename T> 
void f(T&& t);
  • 传左值 int&T&& = int& && → int&
  • 传右值 intT&& = int&&

这就是为什么万能引用既能接收左值,又能接收右值