Why, Really, Deleting an Incomplete Type Is Undefined Behaviour

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

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.

Why is the deletion of an incomplete type defined as undefined behaviour?

The expression delete p; does two things:

  1. Destroy the complete object which contains *p.
  2. Deallocate the memory used to store said object.

Item 2 may be possible when all you know is the address of the object, without any further information. The memory allocator only cares about addresses. But determining the complete object's address may be difficult; you essentially need to promise that you are actually providing the address of a complete object.

But there's more. Before deallocating the object's storage, you must run destructors (Item 1). If the destructor has no effect, then it is acceptable to not run destructors, since that has the same behaviour as if you did run them. But if running destructors does have an effect, omitting Item 1 leads do undefined behaviour, and you need to know the complete type in order to know how to run destrutors. Incidentially, you also need to know the complete type in order to determine the address of the most-derived object for Item 2.

how destructor is functioning when deleting a object of one class in other class

The problem is that destruct::destructObj needs to see the definition of Base to know that there is a destructor to call, but all it has is a forward declaration.

Change your code to this and it works.

#include <iostream>
using namespace std;
class base;

class destruct{
public :
destruct() {
cout<<"Destruct Constructor called"<<endl;
}

void destructObj(base* obj);

~destruct() {
cout<<"Destruct Destructor called"<<endl;
}
};

class base {
int runs;
public:

base(){

cout<<"Constructor called"<<endl;
}

~base(){
cout<<"destructor called"<<endl;
}
};

// *** this function after base has been defined ***
inline void destruct::destructObj(base* obj) {
delete obj;
}

int main() {
base *obj = new base();
destruct *desObj = new destruct();
desObj->destructObj(obj);
return 0;
}

Now this surprises me and I'm wondering if it's a g++ extension. I would have expected a compiler error when you tried to delete a pointer to a class where there is only a forward declaration. But I might be wrong.

UPDATE: apparently this (deleting a pointer to an incomplete class) is undefined behaviour, which I guess implies that it should compile. However your compiler really should be giving you a warning to tell you that there's a problem. Always pay attention to compiler warnings, and always compile with the maximum possible number of warnings enabled.

Delete objects of incomplete type

From the standard [expr.delete]:

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.

So, it's UB if there's nontrivial stuff to do, and it's ok if there isn't. Warnings aren't neccessary for UB.

Could deleting complete class type result in undefined behavior?

There are two prerequisites for undefined behavior in the concerned case:

  1. Object is being deleted through a pointer to an incomplete type;
  2. The complete class of the object being deleted has a non-trivial destructor or a (user-defined) deallocation function.

If either of these conditions is false, then there is no undefined behavior (at least due to the concern being discussed).

This in particular means that

  1. Deleting an object of complete type is safe (because the right destructor and/or deallocation function will be executed).

  2. Deleting an object with a trivial destructor and without a user-defined deallocation function through a pointer to an incomplete type is safe (because in the absence of complete type information the compiler doesn't call the destructor and uses the default deallocation function, which perfectly matches what would happen had the deletion be performed through a pointer to complete type).

Is it legal to call delete on a null pointer of an incomplete type?

The standard says ([expr.delete]/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.

So if T has a non-trivial destructor or has an operator delete overload, you get UB. Nothing is said about the UB being based on the value of the pointer (ie: whether it's a null pointer or not).


On what "object being deleted" mean?

One could consider that "object being deleted" means that this clause only applies to delete calls on actual objects. And therefore, if you pass a null pointer, it does not apply.

First, the rest of the standard discussion about the behavior of delete explicitly calls out that its behavior does not apply to null pointers. [expr.delete]/6&7 both start with "If the value of the operand of the delete-expression is not a null pointer value". Paragraph 5 explicitly does not contain these words. Therefore, we must assume it does apply to null pointers.

Second, what would the meaning of "object being deleted" be if it were passed a null pointer? After all, there is no "object" there.

Well, consider what it means to interpret this text if "object being deleted" talks specifically about the object at the end of that pointer. Well, what happens if you're deleting an array of incomplete classes with non-trivial destructors?

By that logic, this clause does not apply, whether the pointer is null or not. Why? Because the "object being deleted" is of an array type, not a class type. And therefore, this clause cannot apply. Which means that a compiler must be able to invoke delete[] on an array of incomplete classes.

But that's impossible to implement; it would require the compiler to be able to track down code that doesn't exist yet.

So either the "object being deleted" is intended to refer to std::remove_pointer_t<std::decay_t<decltype(expr)>>, or the standard requires behavior that is impossible to implement. The standard wording could probably be cleaned up a bit, replacing "If the object being deleted has incomplete class type at the point of deletion" with "If T is a pointer to U or an array of U, and U has incomplete class type at the point of deletion, ..."

destructing an object with Incomplete type

It's all about the point of instantiation of my_scoped_ptr<Impl>::~my_scoped_ptr.

When you don't provide a user-defined destructor, the default one is defined as soon as the definition of class Holder is processed - basically, it's equivalent to defining the destructor in-class:

class Holder {
// ...
~Holder() {}
};

This destructor needs to destroy _mptr member, so ~my_scoped_ptr is also instantiated at this point, while Impl is still incomplete.

When you explicitly declare the destructor in the header, and define in .cpp file, the instantiation of ~my_scoped_ptr happens at the point of that definition - and by that time, Impl is complete.

std::shared_ptr works around this by capturing the deleter at run-time, in its constructor, at the point where it's handed the raw pointer for the first time, and storing it in the control block. You can even assign std::shared_ptr<Derived> to std::shared_ptr<Base>, and the latter will eventually call the correct destructor, even if non-virtual. std::shared_ptr can pull this trick off because it needs to allocate extra storage (for the reference count, among other things) anyway, so it's already somewhat heavyweight. std::unique_ptr on the other hand exhibits the same issue as your my_scoped_ptr, for all the same reasons.

Purpose of boost::checked_delete

The most common example of an incomplete type is one that has only been declared:

// this file does not include the definition of foo

class foo;

void bad(foo *f)
{
delete f; // undefined behavior if there exists foo::~foo
}

In reality, the definition of foo may look like this:

class foo
{
public:
~foo() { ... };
};

But if the top code has not 'seen' the class definition and just sees the class declaration, the code will compile.

Why does incomplete type of smart pointer data member and raw pointer data member have different behavior when their parent destruct?

Because you forward declaration "CAT" in the header file, you have an incomplete data type. With this information the compiler fall into undefined behavior and the destructor may not be called.

What the standard says

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.

Here you can find a detailed explanation: Why, really, deleting an incomplete type is undefined behaviour?
Just moving the struct declaration to before the class definition should fix your problem:

struct CAT
{
std::string name;
CAT(){std::cout<<"CAT"<<std::endl;}
~CAT(){std::cout<<"~CAT"<<std::endl;}
};

class Widget {
public:
Widget();
~Widget() {
delete pc; // I know we should put this code to cpp
// I am just want to show the difference behavior
// between raw pointer and smart pointer
// when widget object destruct
}
private:
struct Impl;
std::shared_ptr<Impl> pImpl; // use smart pointer
CAT *pc; //raw pointer
};

And the output

Impl
CAT
~CAT
~Impl

Forward declarations are good to speed up compilation time, but can lead to problems when more information about the data type is needed.

But why does it work for smart pointers?
Here's a better explanation: Deletion of pointer to incomplete type and smart pointers

Basically, the shared_ptr only needs the declaration when it initializes or resets the pointer. That means it doesn't need the complete type on the moment of the declaration.

This functionality isn't free: shared_ptr has to create and store a
pointer to the deleter functor; typically this is done by storing the
deleter as part of the block that stores the strong and weak reference
counts or by having a pointer as part of that block that points to the
deleter (since you can provide your own deleter).



Related Topics



Leave a reply



Submit