【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 相比 "左值 / 右值重载",代码更简洁、性能更高、可扩展性更强,是多数场景的最优解。

原著在线阅读地址

相关推荐
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc9 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
ceclar12310 小时前
C++使用format
开发语言·c++·算法
lanhuazui1011 小时前
C++ 中什么时候用::(作用域解析运算符)
c++
charlee4411 小时前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索
老约家的可汗11 小时前
初识C++
开发语言·c++
crescent_悦11 小时前
C++:Product of Polynomials
开发语言·c++
小坏坏的大世界12 小时前
CMakeList.txt模板与 Visual Studio IDE 操作对比表
c++·visual studio
乐观勇敢坚强的老彭12 小时前
c++寒假营day03
java·开发语言·c++
愚者游世12 小时前
brace-or-equal initializers(花括号或等号初始化器)各版本异同
开发语言·c++·程序人生·面试·visual studio