关于删除表达式,C++ 中缺少 "placement delete"

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

我听人说 "C++ doesn't need placement delete because it wouldn't do anything."

考虑以下代码:

#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;
}

正如你在这里看到的,析构函数是虚拟的,都被正确调用,但是 b2 的释放仍然会失败,因为 b2 指向与 c2 不同的地址。

请注意,当使用 placement new[] 构造对象数组时会出现类似的问题,如下所述: Global "placement" delete[]

然而,只需将数组大小保存在内存块的头部并使用单个对象放置在循环中手动处理数组 constructor/destructor 调用,即可轻松解决此问题 new/explicit 析构函数调用。

另一方面,我想不出任何优雅的方法来解决多重继承问题。从删除表达式中的基指针检索原始指针的 "magic" 代码是特定于实现的,并且没有像使用数组那样简单的 "doing it manually" 方法。

这是另一种情况,这成为一个问题,有一个丑陋的 hack 来解决它:

#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;
}

这不是一个真正的问题,更像是一个咆哮,因为我相当确定不存在获取地址的神奇运算符或平台独立方法。在我工作的公司,我们有一个库,它使用自定义分配器和上面的 hack,它工作正常,直到我们不得不 link 它静态地与另一个需要替换全局 new/delete 的程序。我们当前的解决方案是简单地禁止通过指向基址的指针删除对象,该基址不能显示为始终具有与最派生对象相同的地址,但这似乎有点不幸。 "ptr->~T(); free(ptr);" 似乎是一个足够常见的模式,许多人似乎认为它等同于删除表达式,但事实并非如此。我很好奇是否还有其他人遇到过这个问题以及他们是如何解决的。

如果p指向多态class类型的对象,则可以使用dynamic_cast<void*>(p)获取最派生对象的地址。因此你的 customDelete2 可以实现如下:

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));
}

(是的,您可以动态分配 const 个对象。)

因为这只会针对多态 class 类型进行编译,您可能希望将 dynamic_cast 删除为辅助函数:

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));
}