本周小贴士#116: 拷贝消除与值传递

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本周小贴士#116: 拷贝消除与值传递相关的知识,希望对你有一定的参考价值。

作为totW#117最初发表于2016年6月8日

由Geoff Romer(gromer@google.com)创作

更新于2020年6月1日

“一切都如此遥远,一份的一份的一份。万物的距离,你什么都碰不到,什么也碰不到你。”——Chuck Palahniuk

假定你有个这样的类:

class Widget {
 public:private:
  string name_;
};

你是如何写它的构造函数呢?好多年,答案一直是这样做的:

// First constructor version
explicit Widget(const std::string& name) : name_(name) {}

然而,这里有一种更加通用的替代方法:

// Second constructor version
explicit Widget(std::string name) : name_(std::move(name)) {}

(如果你不熟悉std::move,请参见TotW#77,或者假定我们使用std::swap替代;适用相同的原则)。这里发生了什么?通用拷贝来传递std::string不是非常昂贵的吗?事实证明不是的,有时通过值传递(正如我们将看到的,它不是真的“通过拷贝”)可能比引用传递更快。

为了理解为什么,考虑像这样的调用点发生了什么:

Widget widget(absl::StrCat(bar, baz));

使用Widget构造函数的第一个版本,absl::StrCat()产生一个包括串联字符的临时字符串,该字符串通过引用传递到Widget(),然后该字符串被拷贝到name_中。使用Widget构造函数的第二个版本,临时字符串通过值的方式被传递到Widget(),你可能认为这种情况会导致字符串拷贝,但是此处产生了魔法。当编译器看到一个临时对象被用作拷贝构造一个对象时,编译器会简单地为临时对象和新对象使用相同的存储空间,因此一个对象拷贝到另一个对象确实是免费的;这被称为拷贝消除。由于这种优化,字符串永远不会被拷贝,而只会被移动一次,这是一种代价小的固定时间操作。

考虑下参数不是临时对象时会发生什么:

string local_str;
Widget widget(local_str);

在这种情况下,两个版本的代码拷贝字符串,但是第二个版本也会移动字符串。这又是一种代价小的固定时间操作,然而拷贝是一种线性时间操作,因此,在许多情况下这将是一种值得付出的代价。

让这种技术生效的name参数的关键属性是必须拷贝name。事实上,这种技术的本质是在试图在函数调用边界上产生拷贝操作,在那里它可以被消除,而不是在函数内部。这不一定涉及std::move();例如,如果函数需要改变拷贝而不是存储它,那么它只需要在原地改变就行。

何时使用拷贝消除

按值传递参数有几个需要记住的缺点。首先,它使函数体更复杂,这会产生维护和可读性负担。例如,在上面的代码中,我们已经添加std::move调用,这会产生意外访问移动后值的风险。在这个函数中,此风险非常小,但是如果这个函数更复杂,这风险也将更高。

其次,它会降低性能,有时会以令人惊讶的方式。如果没有特定的负载分析,有时很难区分是否有净性能的输赢。

  • 如上所述,这种技术只应用于需要拷贝的参数;当参数不需要拷贝或只是有条件的拷贝时,最好情况它是无效,最坏情况它是有害的。
  • 这种技术通常涉及函数休中的一些额外工作,例如上面的示例中的移动赋值。如果额外的工作带来更多的开销,那么在拷贝不能被消除的情况下的降速可能不值得在拷贝能够被消除的情况下来加速。请注意,这个裁定可能取决于你用例的特定情况。例如,如果Widget()的参数几乎一直是非常短,或者几乎不是临时的,这种技术在平衡上可能是有害的。与往常一样,在考虑优化权衡时,如有疑问,请进行测量。
  • 在问题中的拷贝是一个拷贝赋值时(例如我们想加一个set_name()方法进Widget),传递引用版本有时可以通用复用name_的现有缓存来避免内存分配,在这种情况下,值传递会分配新的内存。另外,值传递一直替换name_的分配的这一事实可能会导致更糟的分配行为:如果name_字段在被设置后易于随着时间增长,那么这种增长将需要进一步的在值传递的情况下的新的分配,然而在按引用传递的情况下,我们只在name_字段超过其历史最大大小时才重新分配。

一般来讲,你应该优先选择更简单、更安全更可读的代码,并且只在你有具体的证据表示复杂版本有更好的性能并且差异很关键时才选择更复杂的代码。这个原则当然适用于这种技术:通过const引用传递更简单且更安全,因此它依然是好的默认选项。但是,如果你工作在对性能敏感的领域,或者你的基准测试显示你正在花费过多时间在拷贝函数参数上,那么按值传递可能是你工具箱中非常有用的工具。

  1. 严格来讲,编译器不需要执行拷贝消除,但是它是如此的强大且极其重要的优化,以至你极不可能遇到不这样做的编译器。

以上是关于本周小贴士#116: 拷贝消除与值传递的主要内容,如果未能解决你的问题,请参考以下文章

本周小贴士#116: Using声明和命名空间别名

本周小贴士#116: Using声明和命名空间别名

本周小贴士#116: 保留对参数的引用

#本周小贴士#77:临时的,移动的,和拷贝的

本周小贴士#143:C++11 删除的函数(= delete)

本周小贴士#101:返回值,引用和生命周期