本章深入探讨了 C++11/14 中最核心也是最复杂的特性:右值引用、移动语义和完美转发。理解这些机制对于编写高效且灵活的现代 C++ 代码至关重要。

条款二十三:理解 std::movestd::forward

  • 本质:它们都是运行期不做任何事情的类型转换函数(Cast)。

  • std::move

    • 功能:无条件地将实参转换为右值(准确地说是右值引用)。

    • 目的:告诉编译器这个对象可以被移动(资源可以被窃取)。

    • 陷阱:对 const 对象使用 std::move 会产生 const 右值,这会导致调用拷贝构造函数而非移动构造函数(因为移动操作需要修改源对象)。

  • std::forward

    • 功能:有条件地将实参转换为右值。仅当实参被绑定到右值时(通过模板参数推导得知),才进行转换。

    • 目的:用于完美转发,保留实参的原始值类别(左值或右值)。

条款二十四:区分通用引用与右值引用

  • 通用引用 (Universal Reference)

    • 形式:必须是 T&&(模板参数 T 需推导)或 auto&&

    • 特性:既可以绑定左值(推导为左值引用),也可以绑定右值(推导为右值引用)。

    • 条件:必须发生类型推导,且形式必须严格为 T&&(不能有 conststd::vector<T>&& 等修饰)。

  • 右值引用 (Rvalue Reference)

    • 形式T&& 但没有类型推导(如具体的类名 Widget&&),或者模板参数未发生推导(如 std::vector<T>::push_back(T&&))。

    • 特性:只能绑定右值。

条款二十五:对右值引用使用 std::move,对通用引用使用 std::forward

  • 原则

    • 也就是在函数内部,将参数传递给其他函数时:

    • 右值引用 参数使用 std::move(因为它总是绑定到右值)。

    • 通用引用 参数使用 std::forward(因为它可能绑定到左值或右值)。

  • 时机:只在最后一次使用该参数时进行转换。如果后续还需要使用该对象,请不要转换,否则对象可能被移走导致状态未定义。

  • 返回值优化 (RVO)

    • 切勿对返回的局部对象使用 std::move(如 return std::move(localObj);)。这会阻止编译器进行 RVO(返回值优化),迫使编译器执行移动操作,反而降低效率。

    • 编译器会自动将返回的局部对象视为右值(如果不能进行 RVO),因此无需手动 move

条款二十六:避免在通用引用上重载

  • 问题:通用引用函数(template<typename T> void f(T&&))是“贪婪”的,它几乎能精确匹配任何类型的参数(除了极少数例外)。

  • 后果

    • 它会劫持你本意想调用的其他重载版本(例如,当传入 short 时,通用引用版本比 int 重载版本更匹配)。

    • 这种问题在构造函数中尤为严重(“完美转发构造函数”),它甚至会劫持拷贝和移动构造函数(当传入非 const 左值时)。

条款二十七:熟悉通用引用重载的替代方法

为了解决条款二十六的问题,可以使用以下替代方案:

  1. 放弃重载:使用不同的函数名。

  2. 传递 const T&:牺牲效率,放弃完美转发。

  3. 传值:对于移动成本低的类型(如 std::string),直接传值可能是一个简单高效的折中方案(结合 std::move)。

  4. Tag Dispatch

    • 通过引入额外的标签参数(如 std::true_type / std::false_type),在内部将通用引用分发到具体的重载实现。

    • 适用于所有参数都需要完美转发,但需要根据参数特性(如是否为整型)选择不同实现的场景。

  5. std::enable_if

    • 使用 SFINAE 技术,在特定条件下“禁用”通用引用模板。

    • 适用于构造函数等无法改名或添加参数的场景。

    • 常用技巧:结合 std::decaystd::is_base_of 来排除特定类型(如类本身及其派生类)。

条款二十八:理解引用折叠

  • 机制:C++ 不允许直接声明“引用的引用”(如 int& &),但在模板实例化和 auto 推导等上下文中,编译器会生成它们。引用折叠规则决定了最终的引用类型。

  • 规则

    • 只要有左值引用参与(& &, & &&, && &),结果就是左值引用 (&)。

    • 只有两个都是右值引用(&& &&),结果才是右值引用 (&&)。

  • 应用:这是通用引用(T&&)和 std::forward 能够工作的底层机制。std::forward 通过模板参数 T 携带的引用信息,利用引用折叠还原出原始的左值或右值类型。

条款二十九:假定移动操作不存在,成本高,未被使用

不要盲目迷信移动语义的性能提升。

  • 不存在:许多旧代码或特定类型(如 std::array)并未提供真正高效的移动操作(std::array 的移动是线性复杂度的)。

  • 成本高:某些操作(如小字符串优化 SSO 的 std::string)移动并不比拷贝快多少。

  • 未被使用

    • 如果移动构造函数未声明为 noexceptstd::vector 等容器在扩容时为了保证强异常安全,会退化为使用拷贝而非移动。

  • 结论:在编写通用模板代码时,应保守估计移动的收益。但在确知具体类型和上下文时,可以利用移动语义进行优化。

条款三十:熟悉完美转发失败的情况

完美转发并不总是完美的,以下情况会导致转发失败(无法通过编译或行为不符合预期):

  1. 花括号初始化器{1, 2, 3} 无法直接推导为 std::initializer_list,导致模板推导失败(除非函数参数明确声明为 std::initializer_list)。

  2. 0NULL 作为空指针:会被推导为 int 而非指针类型,导致转发给指针参数的函数时出错(应使用 nullptr)。

  3. 仅声明未定义的 static const 整数成员:如果通过引用转发,编译器可能需要其地址,从而导致链接错误(需提供定义)。

  4. 重载函数名和模板名:如果仅传递函数名,编译器无法确定选择哪个重载版本或模板实例来实例化通用引用参数(需显式转换或指定类型)。

  5. 位域 (Bitfields):无法对位域进行直接引用(非 const 引用),因为硬件无法寻址到位。需先拷贝到临时变量再转发。