Why, really, deleting an incomplete type is undefined behaviour?

帅比萌擦擦* 提交于 2019-11-27 08:16:05

To combine several answers and add my own, without a class definition the calling code doesn't know:

  • whether the class has a declared destructor, or if the default destructor is to be used, and if so whether the default destructor is trivial,
  • whether the destructor is accessible to the calling code,
  • what base classes exist and have destructors,
  • whether the destructor is virtual. Virtual function calls in effect use a different calling convention from non-virtual ones. The compiler can't just "emit the code to call ~Body", and leave the linker to work out the details later,
  • (this just in, thanks GMan) whether delete is overloaded for the class.

You can't call any member function on an incomplete type for some or all of those reasons (plus another that doesn't apply to destructors - you wouldn't know the parameters or return type). A destructor is no different. So I'm not sure what you mean when you say "why can't it do as it always does?".

As you already know, the solution is to define the destructor of Handle in the TU which has the definition of Body, same place as you define every other member function of Handle which calls functions or uses data members of Body. Then at the point where delete impl_; is compiled, all the information is available to emit the code for that call.

Note that the standard actually says, 5.3.5/5:

if the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

I presume this is so that you can delete an incomplete POD type, same as you could free it in C. g++ gives you a pretty stern warning if you try it, though.

It doesn't know whether the destructor will be public or not.

Calling a virtual method or a non-virtual method are two totally different things.

If you call a non-virtual method, the compiler has to generate code that does this:

  • put all the arguments on the stack
  • call the function and tell the linker that it should resolve the call

Since we're talking about the destructor, there are no arguments to put on the stack, so it looks like we can simply do the call and tell the linker to resolve the call. No prototype needed.

However, calling a virtual method is totally different:

  • put all the arguments on the stack
  • get the vptr of the instance
  • get the n'th entry from the vtable
  • call the function to which this n'th entry points

This is totally different so the compiler really has to know whether you are calling a virtual or non-virtual method.

The second important thing is that the compiler needs to know on which position the virtual method is found in the vtable. For this, it also needs to have the full definition of the class.

Without proper declaration of Body the code in Handle.h does not know whether destructor is virtual or even accessible (i.e. public).

I'm just guessing, but perhaps it has to do with the ability of per-class allocation operators.

That is:

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...!!

And now we've mismatched the allocation operators, and entered undefined behavior. The only way to guarantee that can't happen is to say "make the type complete". Otherwise, it's impossible to ensure.

This is really just a special case of calling a method (the destructor, indirectly). delete impl_ effectively just calls the destructor and then the appropriate operator delete (global or class). You can't call any other function on an incomplete type, so why would delete's call to the destructor be given special treatment?

The part I'm unsure about is what complication causes the standard to make it undefined instead of just forbidding it as in the method call.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!