Search code examples
c++new-operatormultiple-inheritancedelete-operatorplacement-new

Regarding delete expressions the lack of "placement delete" in C++


I have heard people say that "C++ doesn't need placement delete because it wouldn't do anything."

Consider the following code:

#include <cstdlib>
#include <cstdio>
#include <new>

////////////////////////////////////////////////////////////////

template<typename T, typename... ARGS>
T* customNew1(ARGS&&... args) {
    printf("customNew1...\n");
    auto ret = new T { std::forward<ARGS>(args)... };
    printf("OK\n\n");
    return ret;
}

template<typename T>
void customDelete1(T *ptr) {
    printf("customDelete1...\n");
    delete ptr;
    printf("OK\n\n");
}

////////////////////////////////

template<typename T, typename... ARGS>
T* customNew2(ARGS&&... args) {
    printf("customNew2 alloc...\n");
    void *buf = std::malloc(sizeof(T));
    printf("customNew2 construct...\n");
    auto ret = ::new(buf) T { std::forward<ARGS>(args)... };
    printf("OK\n\n");
    return ret;
}

template<typename T>
void customDelete2(T *ptr) {
    printf("customDelete2 destruct...\n");

    // what I want: a "placement delete" which calls the destructor and returns the address that should be passed to the deallocation function
    // e.g.
    //
    // void* ptrToFree = ::delete(ptr);
    // std::free(ptrToFree);
    //
    // equally fine would be a "magic" operator that allows one to obtain said address without actually calling the destructor:
    //
    // void* ptrToFree = get_deallocation_address_of(ptr);
    // ptr->~T();
    // std::free(ptrToFree);

    ptr->~T();
    printf("customDelete2 free...\n");
    std::free(ptr);
    printf("OK\n\n");
}

////////////////////////////////////////////////////////////////

struct A {
    int a;
    A() : a(0) {
        printf("A()\n");
    }
    virtual ~A() {
        printf("~A()\n");
    }
};

struct B {
    int b;
    B() : b(0) {
        printf("B()\n");
    }
    virtual ~B() {
        printf("~B()\n");
    }
};

struct C : A, B {
    int c;
    C() : c(0) {
        printf("C()\n");
    }
    ~C() {
        printf("~C()\n");
    }
};

////////////////////////////////////////////////////////////////

int main() {

    C *c1 = customNew1<C>();
    A *a1 = c1;
    B *b1 = c1;

    // Assume c and a will be the same but b is offset
    printf("c: %x\n", c1);
    printf("a: %x\n", a1);
    printf("b: %x\n", b1);
    printf("\n");

    customDelete1(b1); // <- this will work, the delete expression offsets b1 before deallocing

    printf("--------------\n\n");

    C *c2 = customNew2<C>();
    A *a2 = c2;
    B *b2 = c2;

    printf("c: %x\n", c2);
    printf("a: %x\n", a2);
    printf("b: %x\n", b2);
    printf("\n");

    // customDelete2(b2); // <- this will break
    customDelete2(a2); // <- this will work because a2 happens to point at the same address as c2

    printf("--------------\n\n");

    return 0;
}

As you can see here the destructors, being virtual, are all called properly, but the deallocation of b2 will still fail because b2 points at a different address than c2.

Note that a similar problem arises when one uses placement new[] to construct an array of objects, as described here: Global "placement" delete[]

However this can be worked around without much trouble by simply saving the array size at the head of your block of memory and handling the array constructor/destructor calls manually in a loop using single object placement new/explicit destructor calls.

On the other hand, I cannot think of any graceful way to solve the problem with multiple inheritance. The "magic" code which retrieves the original pointer from the base pointer within the delete expression is implementation specific, and there's no simple way of "doing it manually" like you can with arrays.

Here is another situation where this becomes a problem, with an ugly hack to work around it:

#include <cstdlib>
#include <cstdio>
#include <new>

////////////////////////////////////////////////////////////////

// imagine this is a library in which all allocations/deallocations must be handled by this base interface
class Alloc {
public:
    virtual void* alloc(std::size_t sz) =0;
    virtual void free(void *ptr) =0;
};

// here is version which uses the normal allocation functions
class NormalAlloc : public Alloc {
public:
    void* alloc(std::size_t sz) override final {
        return std::malloc(sz);
    }
    void free(void *ptr) override final {
        std::free(ptr);
    }
};

// imagine we have a bunch of other versions like this that use different allocation schemes/memory heaps/etc.
class SuperEfficientAlloc : public Alloc {
    void* alloc(std::size_t sz) override final {
        // some routine for allocating super efficient memory...
        (void)sz;
        return nullptr;
    }
    void free(void *ptr) override final {
        // some routine for freeing super efficient memory...
        (void)ptr;
    }
};

// etc...

////////////////////////////////

// in this library we will never call new or delete, instead we will always use the below functions

// this is used instead of new...
template<typename T, typename... ARGS>
T* customNew(Alloc &alloc, ARGS&&... args) {
    printf("customNew alloc...\n");
    void *buf = alloc.alloc(sizeof(T));
    printf("customNew construct...\n");
    auto ret = ::new(buf) T { std::forward<ARGS>(args)... };
    printf("OK\n\n");
    return ret;
}

// um...
thread_local Alloc *stupidHack = nullptr;

// unfortunately we also have to replace the global delete in order for this hack to work
void operator delete(void *ptr) {
    if (stupidHack) {
        // the ptr that gets passed here is pointing at the right spot thanks to the delete expression below
        // alloc has been stored in "stupidHack" since it can't be passed as an argument...
        printf("customDelete free @ %x...\n", ptr);
        stupidHack->free(ptr);
        stupidHack = nullptr;
    } else {
        // well fug :-D
    }
}

// ...and this is used instead of delete
template<typename T>
void customDelete(Alloc &alloc, T *ptr) {
    printf("customDelete destruct @ %x...\n", ptr);
    // set this here so we can use it in operator delete above
    stupidHack = &alloc;
    // this calls the destructor and offsets the pointer to the right spot to be dealloc'd
    delete ptr;
    printf("OK\n\n");
}

////////////////////////////////////////////////////////////////

struct A {
    int a;
    A() : a(0) {
        printf("A()\n");
    }
    virtual ~A() {
        printf("~A()\n");
    }
};

struct B {
    int b;
    B() : b(0) {
        printf("B()\n");
    }
    virtual ~B() {
        printf("~B()\n");
    }
};

struct C : A, B {
    int c;
    C() : c(0) {
        printf("C()\n");
    }
    ~C() {
        printf("~C()\n");
    }
};

////////////////////////////////////////////////////////////////

int main() {

    NormalAlloc alloc;

    C *c = customNew<C>(alloc);
    A *a = c;
    B *b = c;

    printf("c: %x\n", c);
    printf("a: %x\n", a);
    printf("b: %x\n", b);
    printf("\n");

    // now it works
    customDelete(alloc, b);

    printf("--------------\n\n");

    return 0;
}

This isn't a question really more of just a rant as I'm fairly sure that no magic operator or platform independent method to obtain the address exists. At the company where I work we had a library that used custom allocators with the hack above which worked okay until we had to link it statically with another program that needed to replace global new/delete. Our current solution is simply to ban the deleting of an object through a pointer to a base that can't be shown to always have the same address as the most derived object, but this seems a bit unfortunate. "ptr->~T(); free(ptr);" seems to be a common enough pattern and many people seem to think it's equivalent to a delete expression, but it's not. I'm curious if anyone else has encountered this problem and how they managed to solve it.


Solution

  • If p points to an object of polymorphic class type, you can get the address of the most derived object using dynamic_cast<void*>(p). Thus your customDelete2 can be implemented as follows:

    template <class T>
    void customDelete2(const T *ptr) {
        const void* ptr_to_free = dynamic_cast<const void*>(ptr);
        ptr->~T();
        std::free(const_cast<void*>(ptr_to_free));
    }
    

    (Yes, you can dynamically allocate const objects.)

    Since this will only compile for a polymorphic class type, you might want to remove the dynamic_cast to a helper function:

    template <class T>
    const void* get_complete_object_address(const T* p, std::true_type) {
        return dynamic_cast<const void*>(p);
    }
    
    template <class T>
    const void* get_complete_object_address(const T* p, std::false_type) {
        return p;
    }
    
    template <class T>
    void customDelete2(const T *ptr) {
        const void* ptr_to_free = get_complete_object_address(
            ptr,
            std::integral_constant<bool, std::is_polymorphic<T>::value>{}
        );
        ptr->~T();
        free(const_cast<void*>(ptr_to_free));
    }