为什么析构函数执行两次?

时间:2019-10-27 12:44:11

标签: c++ inheritance visual-studio-2017 destructor pass-by-value

#include <iostream>
using namespace std;

class Car
{
public:
    ~Car()  { cout << "Car is destructed." << endl; }
};

class Taxi :public Car
{
public:
    ~Taxi() {cout << "Taxi is destructed." << endl; }
};

void test(Car c) {}

int main()
{
    Taxi taxi;
    test(taxi);
    return 0;
}

这是输出

Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.

我使用MS Visual Studio Community 2017(对不起,我不知道如何查看Visual C ++版本)。 当我使用调试模式时。我发现按预期离开void test(Car c){ }函数体时执行了一个析构函数。当test(taxi);结束时,出现了一个额外的析构函数。

test(Car c)函数使用值作为形式参数。 转到该功能时会复制汽车。 因此,我认为离开该功能时只会有一辆“汽车被毁”。 但是实际上在离开函数时有两个“汽车被破坏”。(输出中显示的第一行和第二行) 为什么会有两个“汽车被毁”?谢谢。

===============

当我在class Car中添加虚拟函数时 例如:virtual void drive() {} 然后我得到了预期的输出。

Car is destructed.
Taxi is destructed.
Car is destructed.

2 个答案:

答案 0 :(得分:7)

在将taxi切片为函数调用时,Visual Studio编译器似乎有点捷径,具有讽刺意味的是,它导致它执行的工作比预期的多。

首先,它要使用您的taxi并从中复制构建Car,以便参数匹配。

然后,它再次复制Car 以传递值。

当您添加用户定义的副本构造函数时,此行为消失了,因此编译器似乎是出于自身原因(也许在内部,这是一条更简单的代码路径)使用“允许”这一事实因为副本本身是微不足道的。您仍然可以使用非平凡的析构函数观察到这种行为,这有点像反常。

我不知道在什么程度上合法(尤其是自C ++ 17以来),或者为什么编译器会采用这种方法,但是我同意这不是输出会凭直觉期望的。 GCC和Clang都没有这样做,尽管它们可能以相同的方式进行操作,但是在复制副本方面做得更好。我已经注意到,即使VS 2019仍然不能保证淘汰率。

答案 1 :(得分:3)

发生了什么事?

创建Taxi时,还将创建一个Car子对象。当出租车被摧毁时,两个物体都被破坏了。调用test()时,您将按值传递Car。因此,第二个Car被复制构造,并且在离开test()时将被破坏。因此,我们对3个析构函数进行了解释:序列中的第一个和最后两个。

第四个析构函数(即序列中的第二个析构函数)是意外的,我无法使用其他编译器进行重现。

它只能是临时Car,它是Car参数的源。由于直接提供Car值作为参数时不会发生,因此我怀疑这是将Taxi转换为Car的结果。这是意外的,因为每个Car中已经有一个Taxi子对象。因此,我认为编译器确实将不必要的转换成临时文件,并且没有执行避免该临时文件的复制省略。

注释中的说明:

以下是关于语言律师标准的说明,以验证我的主张:

  • 我这里指的转换是构造函数[class.conv.ctor]进行的转换,即根据另一种类型的参数(此处为Taxi)构造一个类(此处为Car)的对象。
  • 此转换然后使用一个临时对象返回其Car值。允许编译器根据[class.copy.elision]/1.1进行复制省略,因为它可以构造要直接返回到参数中的值,而不是构造一个临时值。
  • 因此,如果此临时操作有副作用,那是因为编译器显然没有利用这种可能的复制删除功能。没错,因为复制省略不是强制性的。

分析的实验确认

我现在可以使用相同的编译器来重现您的情况,并进行实验以确认正在发生的情况。

我上面的假设是,编译器使用构造函数转换Car(const &Taxi)选择了次优参数传递过程,而不是直接从Car的{​​{1}}子对象复制构造。

因此,我尝试调用Taxi,但将test()强制转换为Taxi

我的第一次尝试未能成功改善局势。编译器仍使用次佳的构造函数转换:

Car

第二次尝试成功。它也进行强制转换,但是使用指针强制转换以强烈建议编译器使用test(static_cast<Car>(taxi)); // produces the same result with 4 destructor messages 的{​​{1}}子对象,而不创建这个愚蠢的临时对象:

Car

而且令人惊讶:它按预期工作,仅产生3条销毁消息:-)

结论实验:

在最后的实验中,我通过转换提供了一个自定义构造函数:

Taxi

并使用test(*static_cast<Car*>(&taxi)); // :-) 来实现。听起来很傻,但是这也会生成仅显示3个析构函数消息的代码,从而避免了不必要的临时对象。

这导致认为编译器中可能存在导致此行为的错误。这是在某些情况下可能会丢失从基类进行直接复制构造的可能性。