Search code examples
c++undefined-behaviorrtti

Custom destruction and deallocation of new'd object


Given a pointer obtained from a new expression, is it legal C++ to replace the corresponding delete expression with an explicit call to the destructor (or std::destroy_at) followed by a call to operator delete?

In my project I use a custom form of RTTI. For types with dynamically dispatched destructors the delete expression would not call the appropriate destructor but the destructor of the base class. To solve this issue, I 'manually' (through my RTTI machinery) call the correct destructor based on the runtime type of the allocated object, and than call operator delete directly to deallocate the memory.

This solution works well in my project (compiled with clang and msvc), but I wonder wether this invokes undefined behaviour.

About virtual: I could of course archieve the dynamic dispatch of the destructor by making it virtual. This however gives the objects a vtable pointer, which is functionally not needed because I already have runtime type information in the object. So a virtual destructor solves the problem, but a) introduces unnecessary runtime overhead and more importantly b) it solves an already solved problem, so I would consider it a design flaw.

Code example:

With the dynamic dispatch mechanism provided by C++ the problem would be solved by a virtual destructor.

struct Base { virtual ~Base(); };
struct Derived: Base {
    // My data members which want to be properly destroyed 
};
// ...
Base* p = new Derived(/* ... */);
// ...
delete p; // Derived::~Derived() is being called.

However for several reasons I have a custom form of RTTI (which solves problems virtual functions can't solve).

struct Base { protected: int typeID = ID_OF_TYPE_BASE; };
struct Derived1: Base { 
    Base() { typeID = ID_OF_TYPE_DERIVED1; }
};
struct Derived2: Base { 
    Base() { typeID = ID_OF_TYPE_DERIVED2; }
};

Given a pointer p to Base I can dispatch functions based on the actual runtime type of *p, similar to std::visit on std::variant:

visit(p, [](auto* q) { 
    // Here *q is statically of the runtime type of *p, 
    // so either Base, Derived1 or Derived2.
});

This solves the same problems as the virtual keyword (except that class hierarchies are not easily extendable and it's not as pretty to write), so I can dispatch to the correct destructor like so:

visit(p, [](auto* q) { 
    std::destroy_at(q);
});

So I'm asking if the following is legal C++:

Base* p = new Derived1(/* ... */);
// ...
// delete p; // Calls Base::~Base() - potential memory leak because subobjects of *p are not destroyed. 
visit(p, [](auto* q) {
//  std::destroy_at(q); // Calls Derived1::~Derived1()
//  ::operator delete(q); // Deallocate through pointer to derived type
    // SOLUTION: Simply delete through pointer to derived type!
    delete q; 
});

I'm not asking for advice wether using custom RTTI replacements is reasonable.

I also don't want to imply that using raw new and delete is good practice, this is of course hidden behind unique_ptr's.

The actual code: https://github.com/chrysante/scatha/blob/main/lib/Common/UniquePtr.h


Solution

  • In C++ there is a (new) saying that every line of code can be found to have undefined behavior. Even things like "p++" can be UB. So being legal or not is quite a challenge in C++.

    Now, does it work? If you are not doing anything out of the ordinary, it probably is, yes. I have been doing that for the past 15 years or so. I also do custom RTTI with memory pools and it works flawlessly.

    Just for the sake of teaching, I wrote an example that uses intrusive pointers with custom delete on top of memory pools. You can catch it on Github.

    The only requirement that can come to my mind in general is that if your classes are polymorphic and you are deleting using a pointer to the base class, the base class has to be virtual.