【Effective Modern C++】第五章 右值引用、完美转发和移动语义:25. 对右值引用使用move,对通用引用使用forward

右值引用用 std::move,通用引用用 std::forward

  • 右值引用(T&&,仅绑定右值):只能绑定 "可以被移动" 的对象(比如临时对象、被 std::move 转换的左值),你能确定这个对象的所有权可以被转移。
  • 通用引用(T&&,模板中):也叫转发引用,可能绑定左值(如局部变量)或右值(如临时对象),无法提前确定是否能移动。

基于这两个特性,正确用法是:

  1. 右值引用 :无条件用std::move转换为右值(因为它绑定的对象一定可以移动)。
    示例(Widget 的移动构造函数):
c++ 复制代码
    class Widget {
    public:
        Widget(Widget&& rhs)  // rhs是右值引用
        : name(std::move(rhs.name)),  // 无条件移动
          p(std::move(rhs.p)) {}
    
    private:
        std::string name;
        std::shared_ptr<int> p;
    };

解释:rhs 绑定的是可移动的对象,用 std::move 把它的内部成员 "转移" 到新 Widget 中,避免拷贝。

  1. 通用引用 :有条件用std::forward<T>转换为右值(只有当它绑定的是右值时,才转成右值;绑定左值时保持左值)。
    示例(WidgetsetName 方法):
c++ 复制代码
    class Widget {
    public:
        template<typename T>
        void setName(T&& newName)  // newName是通用引用
        { name = std::forward<T>(newName); }  // 有条件转发
    
    private:
        std::string name;
    };

解释:如果传入的是左值(比如局部变量 n),forward 会保留其左值属性,执行普通赋值;如果传入的是右值(比如临时字符串),forward 会转成右值,执行移动赋值。

错误用法的后果

1. 通用引用上误用 std::move

如果在通用引用上用std::move(而非 std::forward),会无条件把绑定的左值转成右值,导致原本只想 "只读" 的左值被意外移动,原对象值变为未定义。

示例:

c++ 复制代码
// 错误的setName实现
template<typename T>
void setName(T&& newName) {
    name = std::move(newName);  // 无条件移动!
}

// 调用方代码
std::string n = "test";
w.setName(n);  // 本意是传递n的值,结果n被移动空了!
std::cout << n;  // 输出未知(n已被移空)

调用方以为只是 "读" n,结果 n 的所有权被偷走,完全不符合预期。

2. 右值引用上误用 std::forward

虽然语法上可行,但std::forward需要显式指定模板参数(如forward<T>(rhs)),代码更长、容易写错,不如std::move简洁,因此建议避免。

"重载左值 / 右值引用" 替代方案的缺点

如果不用通用引用,直接重载左值和右值版本的 setName 。比如:

c++ 复制代码
// 重载版本
void setName(const std::string& newName) { name = newName; }  // 左值版
void setName(std::string&& newName) { name = std::move(newName); }  // 右值版

这种方案能避免通用引用的误用,但有 3 个致命问题:

  1. 代码冗余:1 个参数需要 2 个重载,n 个参数需要 2ⁿ个重载,维护成本爆炸;

  2. 性能损耗 :比如传入字符串字面量"Adela Novak"时:

    • 重载版:会先创建临时 std::string 对象,再把临时对象移动到 name 中(多了 "构造 + 析构临时对象" 的开销);
    • 通用引用版:直接把字面量传给 std::string 的赋值运算符,无临时对象,效率更高;
  3. 可扩展性差 :对于不定参数函数(如std::make_shared/std::make_unique),重载完全不可行,只能用通用引用 + std::forward

返回值场景

1. 正确用法:返回右值引用 / 通用引用时,要加 std::move/std::forward

如果函数按值返回,且返回的是绑定到右值引用 / 通用引用的对象,必须用std::move(右值引用)或std::forward(通用引用),避免不必要的拷贝:

c++ 复制代码
// 示例1:返回右值引用
Matrix operator+(Matrix&& lhs, const Matrix& rhs) {
    lhs += rhs;
    return std::move(lhs);  // 移动lhs到返回值,避免拷贝
}

// 示例2:返回通用引用
template<typename T>
Fraction reduceAndCopy(T&& frac) {
    frac.reduce();
    return std::forward<T>(frac);  // 右值则移动,左值则拷贝
}
2. 绝对错误:对满足 RVO 条件的局部对象用 std::move

返回值优化(RVO):如果函数按值返回,且满足两个条件:

  • 局部对象的类型 = 函数返回值类型;

  • 返回的是该局部对象本身(而非引用 / 表达式);

    编译器会直接在 "返回值的内存地址" 上构造局部对象,完全消除拷贝 / 移动。

如果手动给局部对象加std::move,会破坏 RVO 条件(返回的是 "对象的引用" 而非对象本身),导致编译器无法优化,只能执行移动操作(反而变慢)。

反例(别用):

c++ 复制代码
Widget makeWidget() {
    Widget w;  // 局部对象,满足RVO条件
    // ... 配置w
    return std::move(w);  // 错误!破坏RVO,编译器只能移动而非直接构造
}

正确写法(无需任何 move/forward):

c++ 复制代码
Widget makeWidget() {
    Widget w;
    // ... 配置w
    return w;  // 编译器会触发RVO,无拷贝/无移动
}

C++ 标准规定,满足 RVO 条件时,编译器要么消除拷贝,要么自动把局部对象视为右值(隐式 move),所以手动加 std::move 完全多余,还会阻碍优化。

总结

  1. 右值引用用std::move(无条件转右值),通用引用用std::forward(有条件转右值),二者不可混用;
  2. 通用引用上用std::move会意外移动左值,满足 RVO 条件的局部对象返回时用std::move会破坏编译器优化;
  3. 通用引用 + std::forward 相比 "左值 / 右值重载",代码更简洁、性能更高、可扩展性更强,是多数场景的最优解。

原著在线阅读地址

相关推荐
借雨醉东风1 天前
程序分享--常见算法/编程面试题:旋转矩阵
c++·线性代数·算法·面试·职场和发展·矩阵
云泽8081 天前
笔试算法 - 双指针篇(二):四大经典求和题型 + 有效三角形计数问题
c++·算法
十五年专注C++开发1 天前
WaitingSpinnerWidget: 一个高度可配置的自定义Qt等待加载动画组件
开发语言·c++·qt·waitingspinner
qeen871 天前
【数据结构】树的基本概念及存储
c语言·数据结构·c++·学习·
王老师青少年编程1 天前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:种树
c++·算法·贪心·csp·信奥赛·区间贪心·种树
hi_ro_a1 天前
C++ 哈希表封装 unordered_map /unordered_set
数据结构·c++·算法·哈希算法
c++之路1 天前
C++ 动态内存
java·jvm·c++
橘颂TA1 天前
【Linux】读写锁
大数据·linux·开发语言·c++·读写锁
霍田煜熙1 天前
HuoTian的两赛vlog(游记)~(2026.04.26写)
c++·奥数·双赛·vlog
CoderCodingNo1 天前
【CSP】CSP-J 2019 江西真题 | 次大值 luogu-P5682 (适合GESP四、五级及以上考生练习)
开发语言·c++·算法