061-右值引用
右值引用
右值引用只能绑定到临时对象
- 所引用的对象将要被销毁
- 该对象没有其他用户
意味着, 使用右值引用的代码可以自由地接管所引用的对象的资源
变量是左值, 离开作用域才会销毁, 所以不能将一个右值引用直接绑定到一个变量, 即使这个变量是右值引用类型
移动语义
可以销毁一个 std::move
后的源对象, 也可以赋予新值, 但不能使用一个 std::move
后的对象的值
std::move
是一个标准库函数模板。它的目的是显式地将一个左值转换为右值引用,以便可以将其转移到另一个对象中,而不是进行复制。以下是逐步解析代码的细节:
代码解释
1 | template <typename T> |
1. 模板参数 T
T
是一个万能引用类型(也称为转发引用,Forwarding Reference)。它可以绑定到左值或右值。- 在使用时,编译器会根据传入的参数推导出
T
的类型。
2. remove_reference<T>::type
std::remove_reference
是一个类型特征工具,用于移除T
类型中的引用修饰符(左值引用&
或右值引用&&
)。- 如果
T
是int&
,则remove_reference<T>::type
是int
。 - 如果
T
是int&&
,则remove_reference<T>::type
也是int
。 - 如果
T
是int
,则remove_reference<T>::type
还是int
。
- 如果
3. 返回类型 ReturnType
ReturnType
定义为remove_reference<T>::type &&
,它是一个右值引用类型。- 例如,如果
T
是int&
,则ReturnType
是int&&
。 - 如果
T
是int
,则ReturnType
是int&&
。 - 如果
T
是int&&
,则ReturnType
仍是int&&
。
- 例如,如果
4. static_cast<ReturnType>(param)
static_cast
将param
转换为右值引用类型(即ReturnType
)。- 这是实现右值语义的关键,使得调用
std::move
后,可以显式地告知编译器 “此对象可以被移动”,而不是执行复制操作。
理解 std::move
的目的
传递右值语义:
- 左值通常用于赋值或复制,而右值(包括临时对象和
std::move
转换后的对象)可以直接参与移动操作。 std::move
是一个语义标记,它并不会真正地移动数据,而是明确表示这个对象可以被移动。
- 左值通常用于赋值或复制,而右值(包括临时对象和
触发移动构造/移动赋值:
- 当对象被
std::move
转换后,编译器会优先选择移动构造函数或移动赋值运算符(如果它们可用)。
- 当对象被
一个实际例子
1 |
|
分析
std::move(str)
将str
显式转换为右值引用。- 这使得构造
newStr
时使用的是std::string
的移动构造函数,而不是复制构造函数。 - 移动构造函数通常只会转移底层资源的所有权,而不是复制内容,因此效率更高。
为什么 std::move
的实现显得有些复杂?
类型安全:
std::move
需要处理所有可能的类型(包括左值引用、右值引用、非引用类型)。- 使用模板和
remove_reference
可以保证它适用于广泛的场景。
避免额外的拷贝:
- 使用
static_cast
确保转换是高效的,并避免额外的临时对象创建。
- 使用
明确右值语义:
- 编译器无法通过常规代码自动推断出某些左值可以被移动。通过
std::move
显式告知编译器可以安全地应用右值语义。
- 编译器无法通过常规代码自动推断出某些左值可以被移动。通过
无论是左值还是右值, 经过 std::move 转化后, 都变成右值吗
是的,经过 std::move
转换后,无论输入的是左值还是右值,结果都会变成右值引用(准确地说是 T&&
类型,其中 T
是去掉引用后的类型)。
不过,需要特别注意的是,std::move
不会改变对象本身的值类别,它只是返回一个右值引用。这意味着:
- 对象本身仍然是左值:
即使通过std::move
转换,对象本身仍然存在,仍然可以通过左值访问它。 - 调用
std::move
的结果是右值引用:
这是一种“显式标记”,告诉编译器该对象的资源可以安全地被转移。
举例说明
转换的效果
1 |
|
输出
1 | x: 20 |
分析:
x
是左值。std::move(x)
返回的是一个右值引用int&&
,但它仍然指向x
的内存。x
的值可以通过y
被修改,但编译器会认为y
是右值引用,可能触发移动语义。
理解 “右值引用”和“右值”
右值引用不是右值
- 右值引用(
T&&
)是一个可以绑定到右值的引用类型,但它本身是一个左值(因为它有名字)。 - 右值 是不具名的值,例如
10
、临时对象或通过std::move
返回的表达式。
示例
1 |
|
输出
1 | process(int&&): 42 |
总结
std::move
的作用:- 它把传入的对象转换为右值引用,告诉编译器可以对其执行移动操作。
- 它不会改变对象本身的值类别或状态。
左值经过
std::move
转换:- 变为右值引用。
右值经过
std::move
转换:- 仍然是右值引用,但语法上统一为右值处理。
std::move
不会销毁或重置对象:- 对象在经过
std::move
后仍然存在,可以继续访问和使用(尽管其资源可能已被转移)。
- 对象在经过
注意: 在移动构造函数和移动赋值运算符这些类实现代码之外的地方, 只有确信需要进行移动操作且移动操作是安全的, 才使用 std::move
显式地移动对象
基本类型和类类型的移动操作
这两个例子看似相似,但它们在底层行为上有根本的区别,原因在于对象类型及其资源管理方式的不同。
第一例:基本类型 int
1 |
|
分析
std::move
转换的作用:std::move(x)
将x
标记为右值引用,但不会对x
本身的值或状态造成任何改变。int&& y
是一个右值引用,但它仍然直接绑定到x
,共享同一块内存。
操作效果:
- 修改
y
的值实际上就是修改x
,因为y
绑定到x
的内存。 - 因此,输出
x
时,其值已经更新为20
。
- 修改
为什么行为正常:
- 对于基本类型(如
int
),std::move
仅改变类型语义,而不涉及实际的数据移动。 int
是轻量的 POD(Plain Old Data),无需复杂的资源管理。
- 对于基本类型(如
第二例:类类型 std::string
1 |
|
分析
std::move
的作用:- 对于类类型(如
std::string
),std::move
同样只是标记为右值引用。 - 但是,类类型的赋值操作会优先调用移动构造函数或移动赋值运算符(如果可用)。
- 对于类类型(如
移动操作的效果:
std::move(str)
会触发std::string
的移动构造函数。- 在移动过程中,
newStr
接管了str
的底层资源(如堆上的字符串缓冲区)。 - 移动完成后,
str
的资源被转移,其状态通常会被置为“空”或“无效”(例如,其缓冲区指针可能为nullptr
,长度为 0)。
为什么
str
状态不稳定:- c++ 标准没有强制规定移动后对象的具体状态,只要求它可以被安全地析构。
- 因此,移动后的
str
可能变为空字符串、未定义状态,或保持部分有效性,具体取决于标准库的实现。
两者的关键区别
特性 | 第一例(int ) |
第二例(std::string ) |
---|---|---|
类型 | 基本类型(POD) | 类类型(具有资源管理) |
std::move 作用 |
仅改变类型语义,不转移资源 | 转移底层资源所有权 |
是否触发移动操作 | 否 | 是,调用移动构造函数 |
移动后原对象状态 | 完全不变 | 可能为空或实现依赖 |
是否需要资源管理 | 否 | 是,底层资源可能在堆上 |
总结
对于基本类型:
std::move
不涉及资源移动,仅改变语义。- 原变量的状态完全保持。
对于类类型:
std::move
通常触发移动操作,将资源转移到新对象。- 原对象可能被置为“资源空”的状态,具体行为依赖于类的实现。
了解这些区别有助于正确使用 std::move
和理解其作用范围。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Hymns!