Why, really, deleting an incomplete type is undefined behaviour?
Asked Answered
S

6

30

Consider this classic example used to explain what not to do with forward declarations:

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

I understand from the standard that this case is headed towards UB since Body's destructor is non trivial. What I'm trying to understand is really the root cause of this.

I mean, the problem seems to be "triggered" by the fact that Handle's dtor is inline, and so the compiler does something like the following "inline expansion" (almost pseudo-code here).

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

In all translation units (only Handle_user.cpp in this case) where a Handle instance gets to be destroyed, right? I just can't understand this: ok, when generating the above inline expansion the compiler doesn't have a full definition of the Body class, but why cannot it simply have the linker resolve for the impl_->~Body() thing and so have it call the Body's destructor function that's actually defined in its implementation file?

In other words: I understand that at the point of Handle destruction the compiler doesn't even know if a (non-trivial) destructor exists or not for Body, but why can't it do as it always does, that is leave a "placeholder" for the linker to fill in, and eventually have a linker "unresolved external" if that function is really not available?

Am I missing something big here (and in that case sorry for the stupid question)? If that's not the case, I'm just curious to understand the rationale behind this.

Symbolic answered 25/3, 2010 at 16:13 Comment(0)
R
33

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.

Raina answered 25/3, 2010 at 16:38 Comment(7)
Or make Handle a template class, so the call to ~Body is emitted at the point of use, instead of the point Handle is defined, so Body may have been completed in the meantime.Tasset
@Ben: I guess so, although I don't think the questioner is planning for the definition of Body to appear in the caller's TU at all.Raina
@Ben: yeah, I've used template classes in other situations to ensure calls to function using incomplete types where emitted at a point where actually those types had become complete, but as Steve pointed out, in this case I was not interested in "workarounds" but rather in the rationaleSymbolic
just for completeness, my "why can't it do as it always does" was infact a bit unfortunate. I was just implicitly considering that not needing to know the prototype of the called function, it could really leave a placeholder for the linker to fill afterwards without needing any other info, but I was clearly overlooking all the other possible things it actually needs to know for this to happen, as you summarized in this answer. ThanksSymbolic
Sure. There are languages where the compiler only needs to know the function name and the arguments provided by the caller. Dynamic languages, for instance. In fact even in C++ varargs kind of work like that. But other function calls don't, and anyway this is never a vararg.Raina
I don't understand why they (the commitee) make deleting an incomplete class type UB. IMHO it simply should not compile.Belda
@Nils: I suspect they did it because it works for classes with trivial destructors, and they wanted to allow that. I agree that they'd save all this trouble if they disallowed it. Anyone forward-declaring a type would have to also declare a function to delete it instead of using delete, and define that function in the TU with the complete type. Anyway the standard never requires that something doesn't compile, only that compilers must at least warn for ill-formed programs. Which g++, at least, already does, so just use -Werror ;-)Raina
A
7

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

Afb answered 25/3, 2010 at 16:27 Comment(0)
M
6

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.

Mckelvey answered 25/3, 2010 at 16:23 Comment(0)
B
4

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

Biform answered 25/3, 2010 at 16:25 Comment(0)
T
2

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.

Tamboura answered 25/3, 2010 at 16:22 Comment(0)
P
1

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.

Primp answered 25/3, 2010 at 16:45 Comment(1)
Why it's undefined? It's undefined ONLY when the destructor is non-trivial. The language can't forbid it without breaking deletion of forward declared POD types, which IS well defined.Tasset

© 2022 - 2024 — McMap. All rights reserved.