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

qia*_*azi 11 c++ inheritance destructor pass-by-value visual-studio-2017

#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;
}
Run Code Online (Sandbox Code Playgroud)

这是输出

Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.
Run Code Online (Sandbox Code Playgroud)

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

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

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

例如,当我添加一个虚函数时class Carvirtual void drive() {} 然后我得到了预期的输出。

Car is destructed.
Taxi is destructed.
Car is destructed.
Run Code Online (Sandbox Code Playgroud)

Lig*_*ica 7

taxi为函数调用切片时,Visual Studio编译器似乎有点捷径,具有讽刺意味的是,这导致它完成了比人们预期的更多的工作。

首先,它从中获取您taxi并复制构造a Car,以便参数匹配。

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

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

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


Chr*_*phe 3

怎么了 ?

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

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

它只能是Car作为Car参数源临时创建的。Car由于直接提供值作为参数时不会发生这种情况,我怀疑它是为了将 转换TaxiCarCar这是意想不到的,因为每个 中已经有一个子对象Taxi。因此,我认为编译器确实对临时变量进行了不必要的转换,并且没有执行可以避免此临时变量的复制省略。

评论中给出的澄清:

这里参考语言律师标准进行澄清以验证我的主张:

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

分析的实验验证

我现在可以使用相同的编译器重现您的案例,并进行实验来确认发生了什么。

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

所以我尝试调用test()但明确地将 转换TaxiCar.

我的第一次尝试并没有成功改善这种情况。编译器仍然使用次优的构造函数转换:

test(static_cast<Car>(taxi));  // produces the same result with 4 destructor messages
Run Code Online (Sandbox Code Playgroud)

我的第二次尝试成功了。它也进行转换,但使用指针转换是为了强烈建议编译器使用andCar的子对象Taxi,而不创建这个愚蠢的临时对象:

test(*static_cast<Car*>(&taxi));  //  :-)
Run Code Online (Sandbox Code Playgroud)

令人惊讶的是:它按预期工作,只产生 3 条销毁消息:-)

实验总结:

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

 class Car {
 ... 
     Car(const Taxi& t);  // not necessary but for experimental purpose
 }; 
Run Code Online (Sandbox Code Playgroud)

并用 来实现它*this = *static_cast<Car*>(&taxi);。听起来很傻,但这也会生成只显示 3 条析构函数消息的代码,从而避免不必要的临时对象。

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

  • 没有回答问题 (2认同)