为什么删除不完整的类型实际上是未定义的行为?

2024-04-08

考虑这个经典的例子来解释什么not与前向声明有关:

//in Handle.h file
class Body;

class Handle
{
   public:
      Handle();
      ~Handle() {delete impl_;}
   //....
   private:
      Body *impl_;
};

//---------------------------------------
//in Handle.cpp file

#include "Handle.h"

class Body 
{
  //Non-trivial destructor here
    public:
       ~Body () {//Do a lot of things...}
};

Handle::Handle () : impl_(new Body) {}

//---------------------------------------
//in Handle_user.cpp client code:

#include "Handle.h"

//... in some function... 
{
    Handle handleObj;

    //Do smtg with handleObj...

    //handleObj now reaches end-of-life, and BUM: Undefined behaviour
} 

我从标准中了解到,这种情况正走向 UB,因为 Body 的析构函数并不平凡。 我试图理解的实际上是造成这种情况的根本原因。

我的意思是,这个问题似乎是由 Handle 的 dtor 是内联的这一事实“触发”的,因此编译器会执行类似于以下“内联扩展”的操作(这里几乎是伪代码)。

inline Handle::~Handle()
{
     impl_->~Body();
     operator delete (impl_);
}

在所有翻译单元中(仅Handle_user.cpp在这种情况下)Handle 实例会被销毁,对吧? 我只是无法理解这一点:好的,当生成上述内联扩展时,编译器没有 Body 类的完整定义,但为什么它不能简单地让链接器解析为impl_->~Body()那么它是否调用了在其实现文件中实际定义的 Body 析构函数?

换句话说:我知道在 Handle 销毁时,编译器甚至不知道 Body 是否存在(非平凡的)析构函数,但为什么它不能像往常一样做,那就是留下一个供链接器填写的“占位符”,如果该函数确实不可用,最终会有一个链接器“未解析的外部”?

我在这里错过了一些大事吗(在这种情况下,我对这个愚蠢的问题感到抱歉)? 如果情况并非如此,我只是想了解其背后的原理。


要组合多个答案并添加我自己的答案,如果没有类定义,调用代码不知道:

  • 该类是否具有声明的析构函数,或者是否使用默认析构函数,如果是,则默认析构函数是否微不足道,
  • 调用代码是否可以访问析构函数,
  • 存在哪些基类并具有析构函数,
  • 析构函数是否是虚拟的。实际上,虚拟函数调用使用与非虚拟函数不同的调用约定。编译器不能只是“发出代码来调用〜Body”,然后让链接器稍后计算细节,
  • (这刚刚进来,谢谢 GMan)是否delete该类已超载。

由于某些或所有这些原因,您不能在不完整类型上调用任何成员函数(加上另一个不适用于析构函数的原因 - 您不知道参数或返回类型)。析构函数也不例外。所以我不确定当你说“为什么它不能像往常那样做?”时你的意思是什么。

如您所知,解决方案是定义析构函数Handle在 TU 中定义为Body,与定义每个其他成员函数的位置相同Handle它调用函数或使用数据成员Body。然后在那个点delete impl_;编译后,所有信息都可用于发出该调用的代码。

请注意,该标准实际上是说 5.3.5/5:

如果被删除的对象有 此时的类类型不完整 删除并且完整的类有一个 非平凡的析构函数或 解除分配函数,其行为是 不明确的。

我认为这是为了让您可以删除不完整的 POD 类型,就像您可以的那样free不过,如果你尝试的话,g++ 会给你一个非常严厉的警告。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么删除不完整的类型实际上是未定义的行为? 的相关文章

随机推荐