10

我对返回值优化的理解是编译器偷偷地传递了将存储返回值的对象的地址,并对该对象而不是局部变量进行更改。

例如,代码

std::string s = f();

std::string f()
{
    std::string x = "hi";
    return x;
}

变得类似于

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 以及调用者是否需要传递隐藏地址参数?

4

2 回答 2

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中的其他任何地方替换(并且在返回时什么都不做)。

于 2013-09-05T13:40:00.187 回答
4

让我们玩 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 中,返回的对象将被隐式moved 而不是复制,因为它是在简单的 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++ 标准使这些优化成为可选的。

于 2013-09-05T14:03:51.217 回答