移动语义 学习笔记

简介

移动语义是 C++11 引入的一项特性,允许在不进行深拷贝的情况下将一个对象资源的所有权转移给另一个对象,从而提高程序的效率。

左值和右值

C++ 的所有表达式都有两个属性:类型和值类别。

在 C++98 中,表达式的值类别只有左值和右值两种。其中可以取地址的变量属于左值(不一定能赋值,如 const 引用和字符串字面量),不能取地址的临时数值属于右值(如非字符串的字面量,非引用类型的函数返回值等)。

自 C++11 起,所有表达式被分为三种基本值类别:

  • 纯右值:就是 C++98 中的右值
  • 左值:在 C++98 中的左值基础上新增了具名的右值引用
  • 亡值:随移动语义引入,指匿名的右值引用,如返回右值引用的函数表达式。“亡”代表其在完成初始化或赋值任务后就会消亡

除了三种基本类别外,还有两种混合类别:

  • 右值:包含纯右值和亡值,不能作赋值运算符的左操作数
  • 泛左值:包括亡值和左值,可以通过评估确定对象或函数的标识的值

深拷贝和浅拷贝

  • 浅拷贝:由原对象创建一个新对象,但新对象只是一个指向原对象地址的引用
  • 深拷贝:由原对象创建一个新对象,并在内存中开辟一块新区域存放原对象所有内容的复制

浅拷贝容易产生内存管理问题,而深拷贝容易产生效率问题。

左值引用和右值引用

  • 左值引用(C++98):Type& name = lvalue;,相当于给左值取别名
  • 右值引用(C++11):Type&& name = rvalue;,相当于给右值取别名

const 左值引用可以引用右值,而右值引用想引用左值需要通过 std::move()
非 const 左值引用只能引用非常量的左值,非 const 右值引用只能引用非常量的右值

右值引用可以延长即将被销毁的匿名临时变量的生命周期,并使其具名、可取址、可修改。但右值引用更重要的一个作用是帮助实现移动语义和完美转发。

移动语义和 std::move()

移动语义通过移动构造函数移动赋值运算符实现。

用户类通常会隐式生成默认移动构造函数,但对于复杂的类往往需要自己编写移动构造函数和移动赋值运算符,来确保:

  1. 资源通过浅拷贝转移
  2. 移动后原对象不再拥有资源

移动构造函数和移动赋值运算符接收的参数都是一个右值引用,为了区别于拷贝语义,只有在传入参数为右值时才会调用。因此使用移动语义需要依靠 std::move()std::move() 位于头文件 <utility>,作用是将一个左值强制转换为一个右值引用。无论是调用类的移动构造/赋值时还是实现移动构造/赋值时调用成员变量类的移动都可能需要用到 std::move()。个人理解 std::move() 本质上的作用就是触发移动语义。

常见问题

具名的右值引用属于左值

具名的右值引用本身属于左值。因此使用它进行对象构造的时候不会调用移动构造函数,而是会调用拷贝构造函数

1
2
// 此处 a 是左值,需要再转换成右值引用以调用 data 的移动构造
A(A&& a) : data(std::move(a.data)) {}

完美转发和 std::forward()

完美转发是一种编程技巧,指在编写泛型函数时通过模板类型推导保留实参的值类别,以针对不同的情况选择拷贝语义或移动语义。

C++11 中,根据引用折叠规则,在发生类型推导 template<class T> 时,T&& 类型既可以引用左值(T被推导为左值引用),也可以引用右值(T被推导为原始类型),称为万能引用。利用这一特性,可以编写一个转发接口函数,根据推断出的值类别进行选择转发:

1
2
3
4
5
template<class T>
void wrapper(T&& arg)
{
foo(std::forward<T>(arg));
}

此处的 std::forward() 位于头文件 <utility>,作用是根据 T 将参数强制转换为左值引用或右值引用。

引用折叠:类型推导时,若 T 为引用类型,T&& 的右值引用会与原引用根据一套规则进行折叠,例如 int& && 会被折叠为 int&


作者

Cu_OH_2

发布于

2024-05-04

更新于

2024-06-19

许可协议