【问题标题】:Why was destructor executed twice?为什么析构函数被执行了两次?
【发布时间】:2020-02-23 01:06:15
【问题描述】:
#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) 函数使用值作为形式参数。 转到该功能时会复制汽车。 所以我认为离开功能时只会有一个“汽车被破坏”。 但实际上离开函数时有两个“Car is destructed”。(输出中显示的第一行和第二行) 为什么会有两个“汽车被毁”?谢谢。

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

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

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

【问题讨论】:

  • 在将Taxi 对象传递给按值获取Car 对象的函数时,编译器如何处理对象切片可能是个问题?
  • 必须是您的旧 C++ 编译器。 g++ 9 给出了预期的结果。使用调试器确定生成对象的额外副本的原因。
  • 我已经用 7.4.0 版本测试了 g++,用 6.0.0 版本测试了 clang++。他们给出了与 op 的输出不同的预期输出。所以问题可能与他使用的编译器有关。
  • 我用 MS Visual C++ 复制。如果我为Car 添加用户定义的复制构造函数和默认构造函数,那么这个问题就会消失,它会给出预期的结果。
  • 请在问题中添加编译器和版本

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


【解决方案1】:

发生了什么?

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

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

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

cmets 中给出的说明:

这里是关于语言律师验证我的主张的标准的澄清:

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

实验确认分析

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

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

所以我尝试调用test(),但明确地将Taxi 转换为Car

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

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

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

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

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

结束实验:

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

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

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

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

【讨论】:

  • 不回答问题
  • @qiazi 我认为这证实了没有复制省略的临时转换的假设,因为这个临时将在调用者的上下文中从函数中生成。
  • 当说“如果您的编译器不执行复制省略,则从出租车转换为汽车”时,您指的是什么复制省略?首先不应该有需要删除的副本。
  • @interjay 因为编译器不需要基于 Taxi 的 Car 子对象构造一个 Car 临时对象来进行转换,然后将此 temp 复制到 Car 参数中:它可以省略复制并直接从原始子对象构造参数。
  • 复制省略是指标准规定应创建副本,但在某些情况下允许省略复制。在这种情况下,没有理由首先创建副本(对Taxi 的引用可以直接传递给Car 复制构造函数),因此复制省略无关紧要。
【解决方案2】:

看起来 Visual Studio 编译器在为函数调用切片 taxi 时采取了一些捷径,具有讽刺意味的是,这导致它做的工作比预期的要多。

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

然后,它正在复制Car 再次作为传递值。

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

我不知道这在多大程度上是合法的(特别是从 C++17 开始),或者为什么编译器会采用这种方法,但我同意这不是我的输出会直觉地预料到。 GCC 和 Clang 都没有这样做,尽管它们可能以相同的方式做事,但更擅长省略副本。我已经注意到即使是 VS 2019 在保证省略方面仍然不是很好。

【讨论】:

  • 抱歉,这不正是我所说的“如果您的编译器不执行复制省略,则从出租车转换为汽车。”
  • 这是一个不公平的评论,因为通过值传递与通过引用传递以避免切片只是在编辑中添加,以帮助 OP 超越这个问题。然后我的回答不是在黑暗中开枪,从一开始就清楚地解释了它可能来自哪里,我很高兴看到你得出相同的结论。现在看看你的表述,“看起来......我不知道”,我认为这里有同样数量的不确定性,因为坦率地说,我和你都不明白为什么编译器需要生成这个临时值。跨度>
  • 好的,然后删除答案中不相关的部分,只留下一个相关的段落
  • 好的,我删除了令人分心的切片段落,并通过精确引用标准来证明关于复制省略的观点。
  • 你能解释一下为什么临时 Car 应该从 Taxi 复制构造,然后再次复制到参数中吗?为什么编译器在提供普通汽车时不这样做?
猜你喜欢
  • 2013-01-12
  • 2021-11-06
  • 1970-01-01
  • 1970-01-01
  • 2013-11-24
  • 2015-07-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多