5

我希望从这个测试程序中看到来自命名返回值优化 (NRVO) 的复制省略,但它的输出是“地址不匹配!” 所以 NRVO 没有发生。为什么是这样?

// test.cpp
// Compile using:
//      g++ -Wall -std=c++17 -o test test.cpp
#include <string>
#include <iostream>

void *addr = NULL;

class A
{
public:
    int i;
    int j;

#if 0
    ~A() {}
#endif
};

A fn()
{
    A fn_a;

    addr = &fn_a;

    return fn_a;
}

int main()
{
    A a = fn();

    if (addr == &a)
        std::cout << "Addresses match!\n";
    else
        std::cout << "Addresses do not match!\n";
}

笔记:

  1. 如果通过启用#if上述方法定义了析构函数,则 NRVO 确实会发生(并且在其他一些情况下也会发生,例如定义虚拟方法或添加std::string成员)。

  2. 没有定义任何方法,所以 A 是一个 POD 结构,或者在最近的术语中是一个琐碎的 class。我在上面的链接中没有看到明确的排除。

  3. 添加编译器优化(添加到一个更复杂的示例,而不仅仅是简化为空程序!)没有任何区别。

  4. 查看第二个示例的程序集表明,当我期望强制返回值优化 (RVO) 时,甚至会发生这种情况,因此上面的 NRVO 并没有通过获取 in 的地址来fn_a阻止fn()。x86-64 上的 Clang、GCC、ICC 和 MSVC 显示相同的行为,表明此行为是故意的,而不是特定编译器中的错误。

     class A
     {
     public:
         int i;
         int j;
    
     #if 0
         ~A() {}
     #endif
     };
    
     A fn()
     {
         return A();
     }
    
     int main()
     {
         // Where NRVO occurs the call to fn() is preceded on x86-64 by a move
         // to RDI, otherwise it is followed by a move from RAX.
         A a = fn();
     }
    
4

2 回答 2

4

在返回纯右值(第二个示例)的情况下允许这样做的语言规则是:

[类.临时]

当类类型 X 的对象被传递给函数或从函数返回时,如果 X 至少有一个合格的复制或移动构造函数([special]),则每个这样的构造函数都是平凡的,并且 X 的析构函数是平凡的或被删除的,允许实现创建一个临时对象来保存函数参数或结果对象。临时对象分别从函数参数或返回值构造,并且函数的参数或返回对象被初始化,就好像通过使用合格的普通构造函数来复制临时对象(即使该构造函数不可访问或不会被重载选择)执行对象的复制或移动的分辨率)。[注意:授予此权限以允许将类类型的对象传递给寄存器中的函数或从函数返回。——尾注]


为什么 [在某些情况下] 不会发生返回值优化?

规则的动机在引用规则的注释中进行了解释。从本质上讲,RVO 有时效率低于没有 RVO。

如果通过启用上面的#if 来定义析构函数,那么 RVO 确实会发生(并且在其他一些情况下也会发生,例如定义虚拟方法或添加 std::string 成员)。

在第二种情况下,这是由规则解释的,因为只有在析构函数很简单时才允许创建临时对象。

在 NRVO 的情况下,我想这取决于语言实现。

于 2020-07-19T21:36:27.013 回答
2

在许多 ABI 上,如果返回值是一个可简单复制的对象,其大小/对齐方式等于或小于指针/寄存器的大小/对齐方式,那么 ABI 将不允许省略。原因是通过寄存器返回值比通过堆栈内存地址更有效。

请注意,当您获得函数中的对象或返回的对象的地址时,编译器会将对象强制入栈。但对象的实际传递将通过寄存器。

于 2020-07-19T21:35:42.003 回答