g++:在涉及多个翻译单元的情况下,RVO是如何工作的

g++: How RVO works in case that multiple translation units are involved

本文关键字:RVO 何工作 工作 情况下 g++ 单元 翻译      更新时间:2023-10-16

首先请看下面的代码,它由两个翻译单元组成。

--- foo.h ---
class Foo
{
public:
    Foo();
    Foo(const Foo& rhs);
    void print() const;
private:
    std::string str_;
};
Foo getFoo();
--- foo.cpp ---
#include <iostream>
Foo::Foo() : str_("hello")
{
    std::cout << "Default Ctor" << std::endl;
}
Foo::Foo(const Foo& rhs) : str_(rhs.str_)
{
    std::cout << "Copy Ctor" << std::endl;
}
void Foo:print() const
{
    std::cout << "print [" << str_ << "]" << std:endl;
}
Foo getFoo()
{
    return Foo(); // Expecting RVO
}
--- main.cpp ---
#include "foo.h"
int main()
{
    Foo foo = getFoo();
    foo.print();
}

请确保foo.cpp和main.cpp是不同的翻译单位。因此,根据我的理解,我们可以说在翻译单元main.o(main.cpp).中没有getFoo()的实现细节

然而,如果我们编译并执行上面的内容,我看不到"Copy Ctor"字符串,这表明RVO在这里工作。

如果你们中的任何人能告诉我,即使"getFoo()"的实现细节没有暴露在翻译单元main中,这是如何实现的,我将不胜感激。o?

我使用GCC(g++)4.4.6进行了上述实验。

编译器只需要始终如一地工作。

换句话说,编译器必须只查看返回类型,并根据该类型决定返回该类型对象的函数将如何返回值。

至少在一个典型的案例中,这个决定是相当微不足道的。它留出一个寄存器(或可能两个)用于返回值(例如,在通常为EAX或RAX的Intel/AMD x86/x64上)。任何小到可以放入的类型都将返回到那里。对于任何太大而无法容纳的类型,该函数将接收一个隐藏的指针/引用参数,该参数"告诉"它将返回结果存放在何处。请注意,这在完全不涉及RVO/NRVO的情况下适用——事实上,它同样适用于返回struct的C代码,就像它适用于返回一个class对象的C++一样。尽管返回struct在C中可能不像在C++中那么常见,但它仍然是允许的,并且编译器必须能够编译这样做的代码。

实际上有两个单独的(可能的)副本可以消除。一种是编译器可以在堆栈上为保存返回值的本地文件分配空间,然后在返回期间从那里复制到指针引用的位置。

第二种可能是从该返回地址复制到值真正需要结束的其他位置。

第一个在函数本身内部被消除,但对其外部接口没有影响。它最终将数据放在隐藏指针告诉它的任何位置——唯一的问题是它是首先创建本地副本,还是总是直接使用返回点。显然,对于[N]RVO,它总是直接起作用。

第二个可能的拷贝是从那个(潜在的)临时拷贝到值真正需要结束的地方。这是通过优化调用序列而不是函数本身来消除的,即给函数一个指向返回值最终目的地的指针,而不是指向某个临时位置,编译器将从该位置将值复制到其目的地。

main不需要getFoo的实现细节就可以实现RVO。它只是希望在getFoo退出后返回值在某个寄存器中。

getFoo对此有两个选项-在其作用域中创建一个对象,然后将其复制(或移动)到返回寄存器,或者直接在该寄存器中创建对象。这就是发生的事情。

它并没有告诉main在其他地方查找,也不需要。它只是直接使用返回寄存器。

(N)RVO与翻译单位无关。该术语通常用于指两种不同的副本省略,一种可以在函数内部应用(从局部变量到返回值),另一种可以由调用方应用(从返回值到局部变量),它们应该单独讨论。

正确的RVO

这是严格在函数内部执行的,请考虑:

T foo() {
   T local;
   // operate on local
   return local;
}

概念上有两个对象,local和返回的对象。编译器可以对函数进行本地分析,并确定两个对象的生存期都是绑定的:local的生存期只是作为返回值的副本的来源。然后编译器可以将两个变量绑定到一个变量中并使用它

在调用方复制省略

在主叫端,考虑T x = foo();。还有两个对象,即从foo()x返回的对象。编译器可以再次确定生存期是绑定的,并将两个对象放在同一位置。

进一步阅读:

  • 值语义:NRVO

  • 值语义:复制省略