伊利德返回参数

Elide returned parameter

本文关键字:参数 返回      更新时间:2023-10-16

假设我们有一个函数:

Foo bar(Foo&& foo)
{
    // assume `Foo` is move constructible
    return std::move(foo);
}

在此示例中:

// (1)
Foo foo = bar(Foo{});

而这个:

// (2)
Foo foo;
// do something with `foo` so that compiler can't optimize it away
...
Foo foo2 = bar(std::move(foo));

我知道在(2)中,我们几乎肯定无法避免默认构造(foo)和移动构造(返回值为bar),但是编译器可以省略第三个移动构造(从bar的返回值到foo2)。

但是(1)呢?在语义上有两种移动结构(一种从临时参数到bar的返回值,另一种从返回值到foo),其中第二个几乎总是可以省略。但是第一个呢?在示例(1)中,是否有可能某些激进的优化只能使一个构造发生,因此它实际上与以下相同:

Foo foo;

这可能吗?

它可以,但在此意图之外使用并不完全安全,请小心使用它。

你可以使用右值来完全消除构造,但这意味着你的中间函数(即bar)必须传递并接受引用参数。坚持使用右值引用并不难,因此可以这样的事情:

class A { /* ... */ };
A && bar (A && a) {return std::move(a);}
A a1 = A();
A a2 = bar( A() );
A a3 = bar( bar( A() ) );

经验法则:如果按值传递,则只能通过优化删除虚假的中间对象实例,因为编译器必须检查实例的使用情况并重新排列。但是,对于引用,我们直接声明我们需要一个已经可用的对象,并且右值将其扩展到临时对象。

像这样传递引用的"陷阱"是,如果您将引用传递给范围已过期的对象,那么您就遇到了麻烦。(例如,返回对局部变量的右值引用,并不比常规引用好,但这种情况可能会被编译器标记,而右值可能不会)。

A && bad_valid_return() {
    A temp;
    return std::move(temp);
}
A a4 = bad_valid_return(); // not ok, object is destroyed once we return!
A good_valid_return() {
    A temp;
    return std::move(temp);
    // better but we can only remove the result's construction via optimize
}

对于它的价值,你也可以这样做:

A && a5 = good_valid_return();

a5本质上是一个正则变量(它的类型将是A&使用时几乎与A a5相同)。它将保存返回的实际临时,因此临时被构造到位。这甚至可以绕过私有对象成员 - 即使operator=和构造函数是私有的(并且与good_valid_return成为好友),赋值也会起作用a5


关于bar

在一般情况下,这几乎是绝对不允许的,以完全避免移动操作员。将每个"按值"值传递的值视为构造障碍 - 越过该点的值必须产生一个新容器,除非"明显"只需要一个对象。

这些"明显"的情况是返回值优化情况。作为单独的代码单元,函数无法知道如何使用其值,因此在"按值"传递中进行交易的函数必须假定需要一个新对象。

例外是内联 - 函数代码在引用它的地方进行了优化,基本上完全删除了函数调用 - 我希望这是您的 (1) 案例唯一一次优化远离任何额外的对象/移动/副本,而无需使用右值引用直接告诉编译器这就是你打算发生的事情。

看起来确实是(1)中可能发生的情况,但是例如,您至少需要使用GCC进行-O3优化才能发生这种情况。(也许是O2,但我不会这么认为)

如果我

没记错的话,RVO 在这里不适用,原因有两个:

  1. 返回的类型(对 Foo 的 R 值引用)与函数的返回类型不同
  2. 返回的值不是局部变量或临时变量

但是,如果您的复制/移动构造函数没有任何副作用(不适用于 deepmax 的 Foo 类)并且 bar 足够简单,我很确定现代编译器能够内联 bar 并优化除默认构造函数之外的所有内容在 as-if 规则下。