条款29:认识移动操作的缺点
移动可能不可用或效率低,需注意noexcept声明和类型支持。
条款29:认识移动操作的缺点
条款29:认识移动操作的缺点
- 假定移动操作不存在,成本高,未被使用
移动语义简介
C++11 引入移动语义,允许用开销低的移动操作替代高成本的复制操作,从而提升性能。编译器会在满足条件时默认生成移动构造函数和移动赋值操作。
但是:
- 并不是所有类型都支持移动操作。
- 有些移动操作并不比复制操作快。
- 移动操作若未声明
noexcept
,可能导致编译器退化使用复制操作以保证异常安全。
为什么要假定移动操作“不存在”?
在泛型代码或模板中,无法保证传入的类型支持移动操作。很多旧代码和部分第三方库仍未提供移动操作。此时,应该假定移动操作不可用或开销较高,避免对移动的依赖。
移动开销示例对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <string>
#include <vector>
#include <utility>
class Widget {
public:
Widget() { std::cout << "Widget 默认构造\n"; }
Widget(const Widget&) { std::cout << "Widget 拷贝构造\n"; }
Widget(Widget&&) noexcept { std::cout << "Widget 移动构造\n"; }
};
std::vector<Widget> vw1(3);
template<typename T>
void logAndAdd(std::vector<Widget> vec, T&& item) {
vec.emplace_back(std::forward<T>(item));
std::cout << "添加元素\n";
}
int main() {
Widget w;
// 传入左值,调用拷贝构造
logAndAdd(vw1, w);
// 传入右值,调用移动构造
logAndAdd(vw1, Widget());
// 假设 Widget 不支持移动,右值会退化为拷贝
}
- 传入左值:调用拷贝构造,性能开销较大。
- 传入右值且支持移动:调用移动构造,效率更高。
- 不支持移动或移动开销大:即使传入右值,也会调用拷贝构造。
标准容器中的移动差异
std::vector
:数据存储在堆上,移动操作只需要指针的复制和清空,开销非常低,常数时间。std::array
:数据存储在对象内部,移动操作需要逐个元素移动,开销是线性时间。
小字符串优化(SSO)对移动的影响
- 许多
std::string
实现采用 SSO,将短字符串存储在内部缓冲区,不分配堆内存。 - 移动这类短字符串的操作与拷贝开销相近,移动不一定更快。
总结与建议
- 通用代码(如模板)应假定移动操作不可用,不依赖移动优化,做好拷贝准备。
- 已知类型或自己控制的代码,可安全使用移动语义提升性能。
- 对于支持移动的类型,确保移动操作声明为
noexcept
,以允许编译器使用移动而非拷贝。
万能引用相关说明
万能引用(又称通用引用)允许函数模板接受左值和右值,通过 std::forward
实现完美转发。示例如下:
1
2
3
4
template<typename T>
void func(T&& param) {
process(std::forward<T>(param)); // 保持参数左值/右值属性
}
在调用时:
- 传入左值,
T
推导为左值引用,param
实际类型是左值引用。 - 传入右值,
T
推导为非引用类型,param
实际类型是右值引用。
但在实际使用中,若 param
的类型不支持移动,转发右值仍可能退化为拷贝。
本文由作者按照 CC BY 4.0 进行授权