析构函数使用返回值优化调用两次

Destructor called twice with Return Value Optimization

提问人:roi_saumon 提问时间:4/28/2023 更新时间:4/28/2023 访问量:120

问:

为了理解复制省略,我写了一个小例子

#include "iostream"

using namespace std;

struct Foo
{
    Foo() { cout << "constructor was called" << endl; }
    ~Foo() { cout << "destructor was called" << endl; }
};

Foo f()
{
    Foo foo;
    return foo;
}

int main()
{
    
    Foo bla = f();
    return 0;
}

输出:

constructor was called
destructor was called
destructor was called

据我了解,在函数中,对象是直接在分配的内存空间中构造的,因此构造函数只被调用一次。我知道当我们退出函数时,对象超出范围并被调用析构函数。但是为什么析构函数被调用两次呢?当我们退出函数范围时,调用的析构函数是否被调用,即使该对象没有被调用者销毁和使用?f()fooblamain()blafoof()

C++ 析构函数 返回值优化

评论

3赞 NathanOliver 4/28/2023
你的理解是不正确的。您使用的是 NRVO,这是不保证的,而不是 RVO。要使 RVO 更改为或只是freturn Foo{};return {};
3赞 Drew Dormann 4/28/2023
不保证命名变量的复制省略。您的输出指示副本被省略。
1赞 Raymond Chen 4/28/2023
这回答了你的问题吗?返回值优化问题
5赞 Sam Varshavchik 4/28/2023
您忘记记录复制构造函数调用。
1赞 Jesper Juhl 4/28/2023
您是否在启用优化的情况下进行编译?如果不是,编译器没有省略副本也就不足为奇了。

答:

3赞 Karen Baghdasaryan 4/28/2023 #1

我还在代码中添加了复制构造函数的日志记录,以说明会发生什么。

#include "iostream"

using namespace std;

struct Foo
{
    Foo() { cout << "constructor was called" << endl; }
    Foo(const Foo&) {cout << "copy constructor was called" << endl;}
    ~Foo() { cout << "destructor was called" << endl; }
};

Foo f()
{
    Foo foo;
    return foo;
}

int main()
{
    
    Foo bla = f();
    return 0;
}

输出为

constructor was called
copy constructor was called
destructor was called
destructor was called

因此,我们看到,从两个可能的对象构造中,两者都发生了,并且一个使用了。copy-constructor

如果我们打开优化,我们会得到

constructor was called
destructor was called

现在,对象直接在内存空间中构造一次,析构函数被调用一次。这是一个演示

评论

0赞 roi_saumon 4/28/2023
谢谢你的回答。我仍然感到困惑。一旦复制构造函数被调用,我们已经进入了 main() 的范围,所以我们只剩下 bla 对象了。为什么析构函数被调用两次?(我说的是第一个输出)
0赞 Dave S 4/28/2023
在第一个输出中,创建了两个对象。第一个是通过 no arg 构造函数创建的,第二个是通过复制构造函数创建的。这两个物体都必须单独销毁。如果修改日志记录函数以打印 的值,您将看到正在创建和销毁两个单独的对象。this
0赞 roi_saumon 4/28/2023
@DaveS,但是不是必须在第二个创建对象的构造函数之前调用第一个创建的对象的析构函数吗?事实上,一旦我们计算表达式 f(),第一个对象就已经超出了范围,所以它应该在赋值之前被销毁 no?
0赞 Karen Baghdasaryan 4/28/2023
不,当复制发生时,第一个对象仍作为临时对象存在。复制结束后,将调用第一个对象的析构函数。如果在第二个对象的构造函数之前调用第一个对象的析构函数,则第二个对象将没有任何可复制的内容。
1赞 roi_saumon 4/28/2023
哦,对了,这是有道理的。谢谢!我也感到困惑,因为在 MSVC 中,默认情况下应用了 NRVO,要删除它,我们需要编译器选项。MSVC 2022 也是如此,但在 MSVC 2019 中,nrvo 默认处于关闭状态/Zc:nrvo-