【问题标题】:Undefined behaviour with non-virtual destructors - is it a real-world issue?非虚拟析构函数的未定义行为 - 这是现实世界的问题吗?
【发布时间】:2011-05-30 05:01:17
【问题描述】:

考虑以下代码:

class A 
{
public:
  A() {}
  ~A() {}
};

class B: public A
{
  B() {}
  ~B() {}
};

A* b = new B;
delete b; // undefined behaviour

我的理解是 C++ 标准说删除 b 是未定义的行为 - 即,任何事情都可能发生。但是,在现实世界中,我的经验是~A()总是被调用,并且内存被正确释放。

如果 B 引入任何具有自己的析构函数的类成员,它们将不会被调用,但我只对上述简单的情况感兴趣,其中使用继承可能修复一个类方法中的错误源代码不可用。

显然,在非平凡的情况下,这不会是您想要的,但至少是一致的。对于显示的代码,您是否知道任何没有发生上述情况的 C++ 实现?

【问题讨论】:

  • 根据我的经验,未定义并不总是意味着不可预测。但在这种情况下,我不明白该值 - 如果您使用的是 A* 指针,则不会调用任何 B 方法,除非它们是 A 中的虚拟方法。
  • @Mark - 是的,但如果我使用 B* 指针,我认为它不再是未定义的行为?
  • 正确,使用 B* 将消除此代码中所有未定义的行为。调用~B,然后调用~A,然后释放sizeof(B) 内存。
  • 如果涉及多重继承,它可能会爆炸。

标签: c++ destructor undefined-behavior


【解决方案1】:

这是 C++ 标签中一个永无止境的问题:“什么是可预测的未定义行为”。自己轻松解决所有问题:获取每个 C++ 编译器实现并检查可预测的不可预测是否仍然有效。然而,这是你必须自己做的事情。

请发回您发现的内容,这将非常有用。只要不可预测的行为具有一致的和注释的行为。这让编写 C++ 编译器的人很难让任何人关注他的产品。按照惯例,标准化会发生很多,而语言的行为很多未定义。

【讨论】:

  • This is however something you have to do by yourself. :-) 我希望通过在这里提问来避免这种情况......哦,好吧!
  • 对我来说听起来很容易并行化。我猜汉斯的真正意思是,“一些不包括我在内的人必须这样做”。棘手的部分是将您的“所有 C++ 编译器”列表放在一起,因为您必须决定要包含哪些内容以及要省略哪些内容。您想排除像 VC6 这样的旧的垃圾编译器,但您不能只说明显的“所有符合标准的编译器”,因为几乎没有符合标准的 C++ 编译器。此外,有些人还在使用旧的垃圾。有时人们会在 WG21 会议上进行民意调查。如果那里没有人关心编译器,那也没关系。
【解决方案2】:

据我所知,没有不正确的实现。另一方面,如果有一个类,如果你在其中放入一个非 POD,它就会爆炸,这是一种坏事,将其描述为 bug 几乎是不合理的。

此外,您的问题标题具有令人难以置信的误导性。是的,不调用类的析构函数是一个严重现实世界的问题。是的,如果您将输入集限制在极少数的真实世界类中,这不是问题。

【讨论】:

  • 你的意思是少数非现实世界的课程。在现实世界的类中,这类事情也不例外:这是一个严重的问题,而且是真实的,当派生类具有虚拟成员或管理自己的资源时,它会在以后(如果不是现在)咬住。
  • @wilhelmtell:从技术上讲,有些类是 POD,不破坏它们是标准合法的 - 有一个类型特征来确定它。
  • 你有更好的标题吗?我的观点是,编译器往往会按照您的要求(无论多么愚蠢)而不是您想要的,这不会使他们的行为“未定义”。为什么非虚拟析构函数的行为没有“定义”?
  • @Roddy:当然。怎么样,“非虚拟析构函数 - 如果派生是 POD”。它没有被定义,因为它太疯狂了,如果标准定义它,它可能会降低 C++ 的质量。如果您希望某人从您的类继承,您可以创建一个虚拟析构函数。简单。如果添加 0.001% 的极端案例,那么所有新人都会陷入其中。
  • @DeadMG:POD 类没有基础,因此对 POD 的任何宽大处理都不适用于 B。
【解决方案3】:

在非平凡的情况下,B 类几乎总是比 A 大,因为它内部有一个 A 的实例以及 B 的其他成员。当您引入虚拟成员时,情况会变得更糟。

所以虽然 ~A 会被调用,但很容易看出这种事情会导致内存泄漏。这是未定义的行为,不是因为它可能不会调用~A,这基本上是有保证的,而是由内存的管理方式决定的。

【讨论】:

  • 内存管理器通常负责记住您请求的字节数 - (这样free() 不需要传入大小)。除非其他成员涉及进一步的动态分配,否则我认为这不会是现实世界的问题?
  • 为什么会出现内存泄漏?除非B 对象被分配了两个单独的块(一个用于A 部分,一个用于表示B 的其余部分,如果不是不可能的话,这是极不可能的),那么将有一个单独的分配,然后delete 调用(错误的)析构函数,该单个块将被释放。哪里漏了?
  • 是的,这是理论上发生的事情,但是由于您告诉编译器这是 A 的一个实例,因此行为仍然是未定义的。如果必须,请依赖 UB,但它可能会无缘无故停止工作,而且它可能不便携。
【解决方案4】:

“对于显示的代码”,您不太可能找到会起作用的实现。也就是说,如果我们不考虑各种“调试”实现,这些实现是专门为捕获此类错误而设计的。

【讨论】:

  • “调试实现”,如果你的意思是运行时工具,如 UBsan,甚至没有必要:g++ 将在编译时发出警告,如果它可以识别它。而且它也不一定足够 ...作为我在这台辅助机器上g++ 中的UBsan 版本(诚然相当旧,来自Debian Jessie 的4.9.2)没有注意到UB 删除就在它面前发生。要点是:这是可以并且必须在编译时捕获的东西,因为之后的任何事情都为时已晚。当然,这只能是一个警告,因为如果*baseBase,则没有UB
猜你喜欢
  • 2015-08-22
  • 1970-01-01
  • 2019-03-13
  • 1970-01-01
  • 2011-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多