Consider this classic example used to explain what not to do with forward declarations:
//in Handle.h file
class Body;
class Handle
{
public:
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:
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:
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.
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.
It doesn't know whether the destructor will be public or not.
To combine several answers and add my own, without a class definition the calling code doesn't know:
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.
Without proper declaration of Body
the code in Handle.h
does not know whether destructor is virtual
or even accessible (i.e. public).
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.