【问题标题】:g++: How RVO works in case that multiple translation units are involvedg++:在涉及多个翻译单元的情况下,RVO 如何工作
【发布时间】:2012-07-23 15:05:44
【问题描述】:

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

--- 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() 的实现细节。

但是,如果我们编译并执行上面的代码,我看不到表明 RVO 在这里工作的“Copy Ctor”字符串。

如果你们中的任何人请告诉我如何实现这一点,即使“getFoo()”的实现细节没有暴露给翻译单元 main.o,我们将不胜感激?

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

【问题讨论】:

  • 使用 -O0 标志编译示例。
  • 你允许编译器使用 C++11 特性吗?
  • 放置 -O0 标志不会改变这种情况。 RVO 似乎在这种情况下有效。根据我的理解,我使用的编译器不允许使用 C++11 功能。

标签: c++ g++ rvo


【解决方案1】:

编译器必须始终如一地工作。

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

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

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

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

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

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

【讨论】:

  • 感谢您对我的问题的明确回答。现在明白了,在不知道实现细节的情况下,编译器可以决定如何存储返回值的策略。
【解决方案2】:

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

getFoo 对此有两个选择 - 在其范围内创建一个对象,然后将其复制(或移动)到返回寄存器,或者直接在该寄存器中创建对象。会发生什么。

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

【讨论】:

  • 感谢您的解释。如果我将类对象大小扩大到无法放入返回寄存器的大小,会发生什么?即使在这种情况下,我们也可以期待 RVO 发生?
  • @Smg 是的,它可能返回对象的指针或引用。见杰瑞的回答,
【解决方案3】:

(N)RVO 与翻译单元无关。该术语通常用于指代可以在函数内部(从局部变量到返回值)和由调用者(从返回值到局部变量)应用的两种不同的复制省略,应该讨论它们分开。

正确的 RVO

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

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

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

在调用方复制省略

在调用方,考虑T x = foo();。同样有两个对象,来自foo()x 的返回对象。编译器可以再次确定生命周期是绑定的,并将两个对象放在同一位置。

进一步阅读:

【讨论】:

  • 我最初认为 RVO 应该与翻译单元有关,因为它会在调用方处理返回值。但现在我可以理解它与翻译单元没有任何关系。并感谢您提供的宝贵资料,这些资料充分解释了我想知道的确切内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-18
相关资源
最近更新 更多