28

考虑这个用于解释前向声明不能做什么的经典示例:

//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
} 

我从标准中了解到,由于 Body 的析构函数并非微不足道,因此该案例将走向 UB。我试图理解的是真正的根本原因。

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

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

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

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

我在这里错过了什么大事吗(在那种情况下,对不起这个愚蠢的问题)?如果不是这样,我只是想了解这背后的基本原理。

4

6 回答 6

31

结合几个答案并添加我自己的,没有类定义调用代码不知道:

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

由于部分或全部原因,您不能在不完整类型上调用任何成员函数(加上另一个不适用于析构函数的原因——您不知道参数或返回类型)。析构函数也不例外。因此,当您说“为什么不能像往常一样?”时,我不确定您的意思。

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

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

如果要删除的对象在删除点具有不完整的类类型,并且完整的类具有非平凡的析构函数或释放函数,则行为未定义。

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

于 2010-03-25T16:38:42.403 回答
7

它不知道析构函数是否公开。

于 2010-03-25T16:27:09.773 回答
6

调用虚拟方法或非虚拟方法是完全不同的两件事。

如果您调用非虚拟方法,编译器必须生成执行此操作的代码:

  • 将所有参数放入堆栈
  • 调用函数并告诉链接器它应该解析调用

由于我们正在讨论析构函数,因此没有参数可以放入堆栈,所以看起来我们可以简单地进行调用并告诉链接器解决调用。不需要原型。

但是,调用虚方法是完全不同的:

  • 将所有参数放入堆栈
  • 获取实例的 vptr
  • 从 vtable 中获取第 n 个条目
  • 调用第 n 个入口指向的函数

这是完全不同的,所以编译器真的必须知道你是在调用虚拟方法还是非虚拟方法。

第二个重要的事情是编译器需要知道虚方法在 vtable 中的哪个位置。为此,它还需要具有类的完整定义。

于 2010-03-25T16:23:55.257 回答
4

如果没有正确声明Body代码中的Handle.h不知道析构函数是否virtual甚至可访问(即公共)。

于 2010-03-25T16:25:32.273 回答
2

我只是在猜测,但也许这与每类分配运算符的能力有关。

那是:

struct foo
{
    void* operator new(size_t);
    void operator delete(void*);
};

// in another header, like your example

struct foo;

struct bar
{
    bar();
    ~bar() { delete myFoo; }

    foo* myFoo;
};

// in translation unit

#include "bar.h"
#include "foo.h"

bar::bar() :
myFoo(new foo) // uses foo::operator new
{}

// but destructor uses global...!!

现在我们不匹配分配运算符,并输入了未定义的行为。保证不会发生的唯一方法是说“使类型完整”。否则无法保证。

于 2010-03-25T16:22:09.037 回答
1

这实际上只是调用方法(间接调用析构函数)的一种特殊情况。delete impl_ 实际上只调用析构函数,然后调用适当的运算符 delete(全局或类)。您不能在不完整类型上调用任何其他函数,那么为什么要对 delete 对析构函数的调用给予特殊处理呢?

我不确定的部分是什么复杂性导致标准使其未定义,而不是像在方法调用中那样禁止它。

于 2010-03-25T16:45:30.733 回答