我对返回值优化的理解是编译器秘密传递将存储返回值的对象的地址,并对该对象而不是局部变量进行更改。
例如,代码
std::string s = f();
std::string f()
{
std::string x = "hi";
return x;
}
变为SIMILAR
std::string s;
f(s);
void f(std::string& x)
{
x = "hi";
}
使用RVO时。这意味着函数的界面已经改变,因为有一个额外的隐藏参数。
现在考虑我从维基百科中偷走的以下案例
std::string f(bool cond)
{
std::string first("first");
std::string second("second");
// the function may return one of two named objects
// depending on its argument. RVO might not be applied
return cond ? first : second;
}
假设编译器将RVO应用于第一种情况,但不应用于第二种情况。但是功能的界面是否会根据是否应用了RVO而改变?如果编译器看不到函数体f
,编译器如何知道是否应用了RVO以及调用者是否需要传递隐藏的地址参数?
答案 0 :(得分:7)
界面没有变化。在所有情况下,结果
该函数必须出现在调用者的范围内;
通常,编译器使用隐藏指针。唯一的
不同的是,当使用RVO时,就像你的第一种情况一样
编译器将“合并”x
和此返回值,构造
x
在指针给出的地址处;什么时候不用
编译器将生成对复制构造函数的调用
return语句,将任何内容复制到此返回值中。
我可能会补充一点,你的第二个例子是而不是非常接近于什么 发生。在呼叫站点,你几乎总能获得一些东西 像:
<raw memory for string> s;
f( &s );
被调用函数将构造一个局部变量 或直接在其通过的地址临时或复制 在这个地址构建一些其他价值。这就是你的最后一次 例如,return语句或多或少是 相当于:
if ( cont ) {
std::string::string( s, first );
} else {
std::string::string( s, second );
}
(显示传递给副本的隐式this
指针
构造函数。)在第一种情况下,如果RVO适用,则特殊
代码将在x
:
std::string::string( s, "hi" );
然后将x
替换为函数中其他位置的*s
(并且在返回时什么都不做。)
答案 1 :(得分:3)
让我们玩NRVO,RVO和复制elision!
这是一个类型:
#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 ++标准使这些优化成为可选项。