Item25:对右值引用使用move,对通用引用使用forward
右值引用用 std::move,通用引用用 std::forward
- 右值引用(T&&,仅绑定右值):只能绑定 “可以被移动” 的对象(比如临时对象、被 std::move 转换的左值),你能确定这个对象的所有权可以被转移。
- 通用引用(T&&,模板中):也叫转发引用,可能绑定左值(如局部变量)或右值(如临时对象),无法提前确定是否能移动。
基于这两个特性,正确用法是:
- 右值引用:无条件用
std::move转换为右值(因为它绑定的对象一定可以移动)。
示例(Widget 的移动构造函数):
1 | class Widget { |
解释:rhs 绑定的是可移动的对象,用 std::move 把它的内部成员 “转移” 到新 Widget 中,避免拷贝。
- 通用引用:有条件用
std::forward<T>转换为右值(只有当它绑定的是右值时,才转成右值;绑定左值时保持左值)。
示例(Widget的setName方法):
1 | class Widget { |
解释:如果传入的是左值(比如局部变量 n),forward 会保留其左值属性,执行普通赋值;如果传入的是右值(比如临时字符串),forward 会转成右值,执行移动赋值。
错误用法的后果
1. 通用引用上误用 std::move
如果在通用引用上用std::move(而非 std::forward),会无条件把绑定的左值转成右值,导致原本只想 “只读” 的左值被意外移动,原对象值变为未定义。
示例:
1 | // 错误的setName实现 |
调用方以为只是 “读” n,结果 n 的所有权被偷走,完全不符合预期。
2. 右值引用上误用 std::forward
虽然语法上可行,但std::forward需要显式指定模板参数(如forward<T>(rhs)),代码更长、容易写错,不如std::move简洁,因此建议避免。
“重载左值 / 右值引用” 替代方案的缺点
如果不用通用引用,直接重载左值和右值版本的 setName 。比如:
1 | // 重载版本 |
这种方案能避免通用引用的误用,但有 3 个致命问题:
代码冗余:1 个参数需要 2 个重载,n 个参数需要 2ⁿ个重载,维护成本爆炸;
性能损耗:比如传入字符串字面量
"Adela Novak"时:- 重载版:会先创建临时
std::string对象,再把临时对象移动到name中(多了 “构造 + 析构临时对象” 的开销); - 通用引用版:直接把字面量传给
std::string的赋值运算符,无临时对象,效率更高;
- 重载版:会先创建临时
可扩展性差:对于不定参数函数(如
std::make_shared/std::make_unique),重载完全不可行,只能用通用引用 +std::forward。
返回值场景
1. 正确用法:返回右值引用 / 通用引用时,要加 std::move/std::forward
如果函数按值返回,且返回的是绑定到右值引用 / 通用引用的对象,必须用std::move(右值引用)或std::forward(通用引用),避免不必要的拷贝:
1 | // 示例1:返回右值引用 |
2. 绝对错误:对满足 RVO 条件的局部对象用 std::move
返回值优化(RVO):如果函数按值返回,且满足两个条件:
局部对象的类型 = 函数返回值类型;
返回的是该局部对象本身(而非引用 / 表达式);
编译器会直接在 “返回值的内存地址” 上构造局部对象,完全消除拷贝 / 移动。
如果手动给局部对象加std::move,会破坏 RVO 条件(返回的是 “对象的引用” 而非对象本身),导致编译器无法优化,只能执行移动操作(反而变慢)。
反例(别用):
1 | Widget makeWidget() { |
正确写法(无需任何 move/forward):
1 | Widget makeWidget() { |
C++ 标准规定,满足 RVO 条件时,编译器要么消除拷贝,要么自动把局部对象视为右值(隐式 move),所以手动加 std::move 完全多余,还会阻碍优化。
总结
- 右值引用用
std::move(无条件转右值),通用引用用std::forward(有条件转右值),二者不可混用; - 通用引用上用
std::move会意外移动左值,满足 RVO 条件的局部对象返回时用std::move会破坏编译器优化; - 通用引用 +
std::forward相比 “左值 / 右值重载”,代码更简洁、性能更高、可扩展性更强,是多数场景的最优解。
