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


12
#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 Carvirtual void drive() {} 然后我得到了预期的输出。

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

3
对象传递给按值Taxi获取Car对象的函数时,编译器如何处理对象切片可能会引起问题?
一位程序员花花公子

1
必须是您的旧C ++编译器。g ++ 9提供了预期的结果。使用调试器确定制作对象的额外副本的原因。
山姆·瓦尔沙夫奇克

2
我已经测试了7.4.0版的g ++和6.0.0版的clang ++。他们给出了与op的输出不同的预期输出。所以问题可能出在他使用的编译器上。
玛丝琳(Marceline)

1
我转载了MS Visual C ++。如果我添加了一个用户定义的copy-constructor和默认的构造函数,Car那么此问题将消失,并且可以得到预期的结果。
interjay

1
请在以下问题中添加编译器和版本
Lightness Races in Orbit”,

Answers:


7

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

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

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

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

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


抱歉,这不是我所说的“如果您的编译器不执行复制省略,则从出租车转换为Car”的意思。
克里斯多夫,

这是不公平的说法,因为仅在编辑中添加了值传递vs参照传递以避免切片的信息,以帮助OP解决此问题。然后,我的回答并非一dark而就,从一开始它就被清楚地解释了,我很高兴看到您得出相同的结论。现在查看您的公式,“看起来……我不知道”,我认为这里存在同样数量的不确定性,因为坦率地说,我和您都不理解为什么编译器需要生成此临时变量。
克里斯多夫,

好吧,然后删除答案中无关的部分,仅在后面留下单个相关的段落
Lightness Races in Orbit

好的,我删除了分散注意力的切片参数,并通过精确引用标准证明了复制省略的意义。
克里斯多夫,

您能解释一下为什么应该从出租车复制临时车,然后再次将其复制到参数中吗?以及为什么在提供普通汽车时编译器不这样做?
克里斯多夫

3

怎么了 ?

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

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

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

评论中给出的说明:

这里澄清了参考语言律师的标准来验证我的主张:

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

分析的实验确认

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

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

因此,我尝试调用,test()但明确将Taxi转换为Car

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

test(static_cast<Car>(taxi));  // produces the same result with 4 destructor messages

我的第二次尝试成功了。它也执行强制转换,但是使用指针强制转换以强烈建议编译器使用和的Car子对象,Taxi而不创建此愚蠢的临时对象:

test(*static_cast<Car*>(&taxi));  //  :-)

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

结论实验:

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

 class Car {
 ... 
     Car(const Taxi& t);  // not necessary but for experimental purpose
 }; 

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

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


2
没有回答问题
轻轨赛

1
@qiazi我认为这证实了无需复制省略就可以进行转换的临时性假设,因为在调用者的上下文中,该临时性将在函数之外生成。
克里斯多夫

1
当说“如果您的编译器不执行复制删除功能,则从出租车转换为汽车”时,您指的是复制复制功能?首先,不应删除任何副本。
interjay

1
@interjay,因为编译器不需要基于Taxi的Car子对象构造Car临时对象来进行转换,然后将这个temp复制到Car参数中:它可以删除副本并直接从原始子对象构造该参数。
克里斯多夫,

1
复制省略是指标准规定应创建副本,但在某些情况下允许删除该副本。在这种情况下,没有理由首先创建副本(Taxi可以直接将引用传递给Car副本构造函数),因此副本省略无关紧要。
interjay
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.