Search code examples
c++memorymultiple-inheritance

How to write a custom deleter that works with multiple inheritance?


I have a program which uses a custom allocator and deallocator to manage memory. I've recently encountered a leak that has lead me down a huge rabbit hole that end with custom deleters being incapable of handling multiple inheritance. In the code sample below:

#include <iostream>
#include <memory>

using namespace std;
 
class Arena {};

void* operator new(std::size_t size, const Arena&) {
    auto ptr = malloc(size);
    cout << "new " << ptr << endl;
    return ptr;
}

void operator delete(void* ptr, const Arena&) {
    cout << "delete " << ptr << endl;
    free(ptr);
}

class A 
{
public: 
    virtual ~A() = default;
};
class B 
{
public: 
    virtual ~B() = default;
};
class AB : public A, public B
{ 
public:
    ~AB() override = default;
};

int main()
{
    B* ptr = new (Arena()) AB;
    ptr->~B();
    operator delete(ptr, Arena());

    return 0;
}

The output is:

new 0x55e20c8a6eb0
delete 0x55e20c8a6eb8
free(): invalid pointer

Because the address of B is a vtable somewhere inside AB. Using the builtin delete ptr function leads to the pointer being returned to it's original value and freed successfully. I've found some information about top_offset being used to address this here, but this is implementation dependent. So, is there a way to convert a pointer to B back into a pointer to AB without knowing anything about AB?


Solution

  • You can do it this way:

    void* dptr = dynamic_cast<void*>(ptr);
    ptr->~B();
    operator delete(dptr, Arena());
    

    Live demo

    Note you need to dynamic_cast before destroying the B object.

    Without RTTI things get hairy. I assume that in your real code you need the identity of the arena object (otherwise it would be trivial to define member operators new/delete that just pull an arena out of thin air and redirect to global placement new/delete). You need to store this identity somewhere. Hmm, if we only could dynamically allocate some memory for it... wait a minute... we are allocating memory, we can store it there, just increase the size appropriately...

    union AlignedArenaPtr {
      Arena* arena;
      std::max_align_t align;
    };
    
    struct Base { // inherit everything from this
    
        virtual ~Base() = default;
    
        void* operator new(std::size_t size, Arena *arena) {
            auto realPtr = (AlignedArenaPtr*)::operator new(size + 
                sizeof(AlignedArenaPtr), arena);
            realPtr->arena = arena;
            return realPtr + 1;
        }
    
        void operator delete(void* ptr) {
           auto realPtr = ((AlignedArenaPtr*)(ptr)) - 1;
           ::operator delete(realPtr, realPtr->arena);
        }
    
        void* operator new(std::size_t size) = delete; // just in case
    };
    

    Live demo