如果没有定义析构函数,为什么 Return 值优化不会发生?

Why does Return Value Optimization not happen if no destructor is defined?

我希望从这个测试程序中看到来自命名 Return 值优化 (NRVO) 的 copy elision,但它的输出是“地址不匹配!”所以 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 结构,或者用最近的术语来说是 a trivial class。我在上面的链接中没有看到对此的明确排除。

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

  4. 查看第二个示例的 assembly 表明即使我期望强制性 Return 值优化 (RVO) 也会发生这种情况,因此没有阻止上面的 NRVO通过在 fn() 中获取 fn_a 的地址。 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();
     }
    

在许多 ABI 上,如果 return 值是一个平凡可复制的对象,其 size/alignment 等于或小于 pointer/register 的值,那么 ABI 将不允许省略.原因是通过寄存器 return 的值比通过堆栈内存地址 更多 效率。

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

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

[class.temporary]

When an object of class type X is passed to or returned from a function, if X has at least one eligible copy or move constructor ([special]), each such constructor is trivial, and the destructor of X is either trivial or deleted, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the eligible trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object). [Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers. — end note ]


Why does Return Value Optimization not happen [in some cases]?

引用规则的注释中解释了该规则的动机。本质上,RVO 有时比没有 RVO 效率低。

If a destructor is defined by enabling the #if above, then the RVO does happen (and it also happens in some other cases such as defining a virtual method or adding a std::string member).

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

在 NRVO 案例中,我想这取决于语言实现。