每周技巧 #11:返回策略
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #11: Return Policy。
原文最初作为 TotW #11 发布于 2012 年 8 月 16 日。
“Frodo:返程用的东西一点也不会剩下。” “Sam:我觉得不会有返程了,Frodo 先生。” – 《指环王:王者归来》(J.R.R. Tolkien 小说;Fran Walsh、Philippa Boyens 和 Peter Jackson 编剧)
注意:这条技巧虽然仍然相关,但它早于 C++11 移动语义的引入。阅读时也请同时记住 TotW #77 中的建议。
许多较老的 C++ 代码库,会表现出某种对复制对象的畏惧。幸运的是,借助一种叫做“返回值优化”(RVO)的机制,我们可以在不实际复制的情况下“复制”。
RVO 是几乎所有 C++ 编译器长期支持的特性。看下面这段 C++98 代码,它有拷贝构造函数和赋值运算符。这些函数非常昂贵,所以开发者让它们每次被使用时都打印一条消息:
|
|
(注意,这里我们有意避开移动操作的讨论。更多信息见 TotW #77。)
如果这个类有下面这样的工厂方法,你会不会被吓得后退一步?
|
|
看起来很低效,对吧?如果运行下面这行会发生什么?
|
|
简单答案:你可能会预期至少创建两个对象:被调用函数返回的对象,以及调用函数中的对象。两者都是拷贝,所以程序会打印两条关于昂贵操作的消息。真实世界里的答案:不会打印任何消息,因为拷贝构造函数和赋值运算符根本没有被调用!
这是怎么发生的?许多 C++ 程序员会写“高效代码”:创建一个对象,把这个对象的地址传给一个函数,让该函数通过指针或引用操作原始对象。好吧,在下面描述的情形下,编译器可以把这种“低效拷贝”转换成那种“高效代码”!
当编译器看到调用函数中的一个变量(将由返回值构造),以及被调用函数中的一个变量(将被返回)时,它会意识到不需要同时存在这两个变量。在幕后,编译器会把调用函数中变量的地址传给被调用函数。
引用 C++98 标准的话:“每当一个临时类对象通过拷贝构造函数被复制时……实现可以把原对象和拷贝视为引用同一个对象的两种不同方式,并完全不执行拷贝,即使该类的拷贝构造函数或析构函数有副作用。对于返回类类型的函数,如果 return 语句中的表达式是一个局部对象的名字……实现可以省略创建用于保存函数返回值的临时对象……”(C++98 标准第 12.8 节 [class.copy] 第 15 段。C++11 标准第 12.8 节第 31 段有类似表述,但更复杂。)
担心“可以”听起来承诺不够强?幸运的是,所有现代 C++ 编译器默认都会执行 RVO,即使在 debug 构建中也是如此,即使函数没有内联也是如此。
如何确保编译器执行 RVO?
被调用函数应该为返回值定义一个单独变量:
|
|
调用函数应该把返回值赋给一个新变量:
|
|
就是这样!
如果调用函数复用一个已有变量来保存返回值,编译器就不能做 RVO(不过,对于支持移动的类型,这种情况下会应用移动语义):
|
|
如果被调用函数用多个变量作为返回值,编译器也不能做 RVO:
|
|
但如果被调用函数只使用一个变量,并在多个位置返回它,那没有问题:
|
|
这大概就是你关于 RVO 需要知道的全部内容。
还有一件事:临时对象
RVO 不只适用于具名变量,也适用于临时对象。当被调用函数返回一个临时对象时,你也可以受益于 RVO:
|
|
当调用函数立即使用返回值(该返回值存储在临时对象中)时,你也可以受益于 RVO:
|
|
最后说明:如果你的代码需要复制,那就复制,无论这些复制能不能被优化掉。不要用正确性换效率。