函数的调用方如何知道是否使用了返回值优化

How does the caller of a function know whether Return Value Optimization was used?

本文关键字:返回值 优化 是否 调用 方如何 函数      更新时间:2023-10-16

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

例如,代码

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

界面没有变化。 在所有情况下,结果函数必须出现在调用方的作用域中;通常,编译器使用隐藏指针。 唯一的不同之处在于,当使用 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(并且在返回时什么都不做)。

让我们玩 NRVO、RVO 和复制 elision!

这是一个类型:

#include <iostream>
struct Verbose {
  Verbose( Verbose const& ){ std::cout << "copy ctorn"; }
  Verbose( Verbose && ){ std::cout << "move ctorn"; }
  Verbose& operator=( Verbose const& ){ std::cout << "copy asgnn"; }
  Verbose& operator=( Verbose && ){ std::cout << "move asgnn"; }
};

这很啰嗦。

这是一个函数:

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++标准使这些优化可选。