让我们玩 NRVO、RVO 和复制省略!
这是一个类型:
#include <iostream>
struct Verbose {
Verbose( Verbose const& ){ std::cout << "copy ctor\n"; }
Verbose( Verbose && ){ std::cout << "move ctor\n"; }
Verbose& operator=( Verbose const& ){ std::cout << "copy asgn\n"; }
Verbose& operator=( Verbose && ){ std::cout << "move asgn\n"; }
};
这很冗长。
这是一个函数:
Verbose simple() { return {}; }
这很简单,并且使用直接构造它的返回值。如果Verbose
缺少复制或移动构造函数,上述函数将起作用!
这是一个使用 RVO 的函数:
Verbose simple_RVO() { return Verbose(); }
在这里,未命名的Verbose()
临时对象被告知将自身复制到返回值。RVO 意味着编译器可以跳过该副本,并直接构造Verbose()
返回值,当且仅当存在复制或移动构造函数时。复制或移动构造函数没有被调用,而是被省略了。
这是一个使用 NRVO 的函数:
Verbose simple_NRVO() {
Verbose retval;
return retval;
}
要发生 NRVO,每条路径都必须返回完全相同的对象,并且您不能偷偷摸摸地处理它(如果您将返回值转换为引用,然后返回该引用,这将阻止 NRVO)。在这种情况下,编译器所做的就是将命名对象retval
直接构造到返回值位置。与 RVO 类似,复制或移动构造函数必须存在,但不会被调用。
这是一个无法使用 NRVO 的函数:
Verbose simple_no_NRVO(bool b) {
Verbose retval1;
Verbose retval2;
if (b)
return retval1;
else
return retval2;
}
由于它可以返回两个可能的命名对象,因此它不能在返回值位置同时构造它们,因此它必须进行实际复制。在 C++11 中,返回的对象将被隐式move
d 而不是复制,因为它是在简单的 return 语句中从函数返回的局部变量。所以至少是这样的。
最后,在另一端有复制省略:
Verbose v = simple(); // or simple_RVO, or simple_NRVO, or...
当你调用一个函数时,你给它提供了它的参数,你告诉它应该把它的返回值放在哪里。调用者负责清理返回值并为其分配内存(在堆栈上)。
这种通信是通过调用约定以某种方式完成的,通常是隐式的(即,通过堆栈指针)。
在许多调用约定下,可以存储返回值的位置最终可以用作局部变量。
通常,如果您有以下形式的变量:
Verbose v = Verbose();
可以省略隐含的副本——Verbose()
直接在其中构造v
,而不是创建一个临时的然后复制到v
。以同样的方式,如果编译器的运行时模型支持它(通常是这样),则可以省略simple
(或,或其他)的返回值。simple_NRVO
基本上,调用站点可以告诉simple_*
将返回值放在特定位置,并将该位置简单地视为局部变量v
。
请注意,NRVO 和 RVO 以及隐式移动都是在函数内完成的,调用者不需要对此一无所知。
类似地,调用站点的省略都是在函数之外完成的,如果调用约定支持它,则不需要函数体的任何支持。
这不必在每个调用约定和运行时模型中都是正确的,因此 C++ 标准使这些优化成为可选的。