条款41 :针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递

ZaynPei Lv6

该条款探讨了一种在特定情况下,可以替代“为左值和右值分别重载”这一常规做法的编码技巧。C++98 的一条重要准则是“避免按值传递用户定义类型”,而该条款则解释了在C++11的移动语义下,这条准则何时可以被“打破”。

当一个函数需要持有其参数的一个副本时(例如,将其存入一个数据成员),在 C++11 中最常见的做法是提供两个重载版本:

  • 一个版本接受 const T&(左值),并在函数体内复制它。
  • 另一个版本接受 T&&(右值),并在函数体内移动它。
    1
    2
    3
    4
    5
    6
    7
    8
    class Widget {
    public:
    // 方案一:为左值和右值分别重载
    void addName(const std::string& newName) { names.push_back(newName); } // 复制左值
    void addName(std::string&& newName) { names.push_back(std::move(newName)); } // 移动右值
    private:
    std::vector<std::string> names;
    };
    这种做法虽然正确且高效,但缺点是需要编写和维护两份几乎相同的代码。

按值传递的替代方案

条款41提出的替代方案是,编写一个单一的、按值传递的函数,并在函数体内 std::move 这个参数副本。

1
2
3
4
5
6
7
8
9
class Widget {
public:
// 方案二:按值传递
void addName(std::string newName) { // newName 是一个副本
names.push_back(std::move(newName)); // 将副本移动到容器中
}
private:
std::vector<std::string> names;
};
这个方案的精妙之处在于它如何利用移动语义来处理不同类型的实参。

当传入一个左值时(例如 std::string name; w.addName(name);):

  • 重载方案:const std::string& 版本被调用。成本是 1 次复制(在 push_back 内部)。
  • 按值传递方案:形参 newName 通过复制构造函数从 name 创建。 (1 次复制); 在 push_back 内部,newName 被移动到 vector 中。(1 次移动). 总成本:1 次复制 + 1 次移动。

当传入一个右值时(例如 w.addName(“hello”);): - 重载方案:std::string&& 版本被调用。成本是 1 次移动(在 push_back 内部)。 - 按值传递方案:形参 newName 通过移动构造函数从右值实参创建。(1 次移动); 在 push_back 内部,newName 被移动到 vector 中。(1 次移动). 总成本:2 次移动。

因此, 与重载方案相比,按值传递方案在处理左值时多了一次移动,处理右值时也多了一次移动。

按值传递的适用条件

既然按值传递会带来额外的移动开销,那么只有在满足一系列严格条件时,它才是一个值得考虑的选项。这些条件都体现在条款的标题中:

形参是可复制的 (Copyable):如果参数是只移类型(如 std::unique_ptr),那么重载方案只需要写一个 T&& 版本即可(成本1次移动),而按值传递方案则需要2次移动,成本翻倍,因此不适用。

移动成本低廉 (Cheap to Move):按值传递方案的额外成本是一次移动。只有当这个移动操作非常廉价时(例如,对于 std::string 或 std::vector 等大多数标准容器),这笔额外开销才是可以接受的。

形参一定会被复制 (Always Copied):函数必须在所有代码路径上都确实需要这个参数的副本。如果函数中存在某个分支(例如一个检查失败的 if 语句)会提前返回而不使用该副本,那么按值传递方案中提前创建副本的开销就被浪费了。

必须避免切片问题 (Slicing Problem):按值传递不适用于多态场景。如果你按值传递一个基类对象,那么当调用者传入一个派生类对象时,其派生类特有的部分将被“切掉”,这通常会导致非预期的行为。

总之, 按值传递并非要颠覆 C++98 中“避免按值传递”的传统智慧。然而,在现代 C++ 中,对于那些可复制、移动成本低、总是需要被复制且不涉及多态的参数,按值传递提供了一种简洁的替代方案。它用一次额外的廉价移动操作,换取了更少的代码量和更简单的维护,避免了函数重载或模板带来的复杂性。

On this page
条款41 :针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递