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



我希望从这个测试程序中看到命名返回值优化(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结构,或者在最近的术语中是一个平凡的类。在上面的链接中,我没有看到对此的明确排除。

  3. 添加编译器优化(对于一个更复杂的例子,它不仅仅简化为空程序!(没有任何区别。

  4. 查看第二个示例的程序集可以发现,当我期望强制的返回值优化(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();
    }
    

在返回prvalue的情况下允许这样做的语言规则(第二个例子(是:

[class.temporary]

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


为什么[在某些情况下]没有进行返回值优化?

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

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

在第二种情况下,这是由规则解释的,因为只有在析构函数微不足道时才允许创建临时函数。

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

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

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

最新更新