非虚析构函数的未定义行为 - 这是一个现实世界中的问题吗?

4

Consider the following code:

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*指针,则除非它们最初就是A中的虚拟方法,否则不会调用任何B方法。 - Mark Ransom
@Mark - 是的,但是如果我使用B*指针,我认为它就不再是未定义行为了? - Roddy
1
使用B*将消除此代码中的所有未定义行为。~B被调用,然后是~A,最后释放sizeof(B)内存。 - Mark Ransom
1
如果涉及到多重继承,它可能会潜在地出现问题。 - Martin York
4个回答

6
这是C++标签中一个永无止境的问题:“什么是可预测的未定义行为”。你可以自己解决这个问题:获取每个C++编译器实现并检查可预测的不可预测是否仍然有效。但这是你必须自己完成的任务。
请回帖告诉我们你发现了什么,这将非常有用。只要不可预测的行为在所有情况下都具有一致和注释的行为。对于编写C++编译器的人来说,这使得让别人关注他的产品变得非常困难。在一个有很多未定义行为的语言中,惯例标准化经常发生。

这是你必须自己完成的事情。:-) 我本来希望通过在这里询问来避免这种情况... 哦,好吧! - Roddy
听起来很容易并行化。我猜汉斯真正的意思是,“除了我之外的一些人必须去做它”。棘手的部分是组合“所有C++编译器”的列表,因为你必须决定包括什么和排除什么。你想排除像VC6这样的旧垃圾编译器,但你不能只说显而易见的“所有符合标准的编译器”,因为几乎没有符合标准的C++编译器。此外,有些人仍在使用旧的垃圾编译器。有时人们会在WG21会议上进行民意调查。如果那里没有人关心一个编译器,那就没关系了。 - Steve Jessop

3
据我所知,几乎所有实现都符合这一点。另一方面,如果您将非POD放入类中导致程序崩溃,这是一件很糟糕的事情,这很难不被认为是一个错误。
此外,您的问题标题非常具有误导性。是的,不调用类的析构函数在真实世界中是一个严重的问题。如果你将输入集限制在极少数的真实世界类中,它就不是一个问题。

1
你指的是少数非真实世界类。在真实世界的类中,这种情况没有例外:这是一个严重的问题,它是真实存在的,如果不是现在,以后会有问题的,当派生类具有虚成员或管理自己的资源时。 - wilhelmtell
@wilhelmtell:从技术上讲,有些类是POD类型,它们的销毁是符合标准的-有一种类型特征可以确定它。 - Puppy
@Roddy 什么?你从哪里听到我说的? - wilhelmtell
1
@Roddy:当派生类是POD时,使用非虚析构函数。这个问题没有定义,因为它太疯狂了,如果标准定义了它,那么它可能会降低C++的质量。如果你想让别人继承你的类,你需要创建一个虚拟析构函数。很容易。如果你添加了0.001%的边角情况,那么所有新手都会被卷入其中。 - Puppy
1
@DeadMG:POD类没有基类,因此对POD的任何宽容都不适用于B。 - Steve Jessop
显示剩余2条评论

1
“对于所示的代码”,很不可能会找到一个会出错的实现。除非我们排除各种“调试”实现,这些实现是专门和有意设计来捕捉此类错误的。

“调试实现”(如果您指的是运行时仪器,例如UBsan),甚至不是必需的:如果可以识别它,g ++将在编译时发出警告。但这也不一定足够……因为我在此次要机器上的g ++中拥有的(诚然相当古老的Debian Jessie 4.9.2)版本的UBsan并没有注意到正在其前面发生的UB删除。重点是:这是必须在编译时捕获的内容,因为之后的任何操作都为时已晚。当然,它必须只是一个警告,因为如果*baseBase,则没有UB。 - underscore_d

1

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

因此,尽管会调用~A,但很容易看出这种情况可能导致内存泄漏。它是未定义的行为,不是因为它可能不调用~A,而是由于内存管理方式的不同。


1
内存管理器通常负责记住您请求的字节数 - (因此free()不需要传递大小)。除非涉及更多动态分配的其他成员,否则我认为这不会是一个真实世界的问题? - Roddy
为什么会出现内存泄漏?除非B对象被分配了两个不同的块(一个用于表示A部分,另一个用于表示剩余的B部分),这是极其不可能的,否则就只有一个分配,而在delete调用错误的析构函数之后,该单个块将被释放。泄漏在哪里? - Rob Kennedy
1
是的,理论上是这样的,但由于您告诉编译器它是 A 的一个实例,行为仍然是未定义的。如果必须依赖未定义行为,则可能会因无明显原因而停止工作,并且可能不具备可移植性。 - Alexander Rafferty

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接