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

标签 c++ new-operator multiple-inheritance delete-operator placement-new

我听人说“C++ 不需要放置删除,因为它什么也做不了。”

考虑以下代码:

#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[]

然而,只需将数组大小保存在内存块的头部并使用单个对象放置新/显式析构函数调用在循环中手动处理数组构造函数/析构函数调用,就可以轻松解决这个问题。

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

这是另一种情况,这成为一个问题,有一个丑陋的 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,它工作正常,直到我们不得不将它静态链接到另一个需要替换全局新建/删除的程序。我们当前的解决方案是简单地禁止通过指向基址的指针删除对象,该基址不能显示为始终具有与最派生对象相同的地址,但这似乎有点不幸。 “ptr->~T();自由(ptr);”似乎是一个足够常见的模式,许多人似乎认为它等同于删除表达式,但事实并非如此。我很好奇是否还有其他人遇到过这个问题以及他们是如何解决的。

最佳答案

如果p指向多态类类型的对象,您可以使用 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 个对象。)

由于这只会针对多态类类型进行编译,您可能需要删除 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));
}

关于c++ - 关于删除表达式,C++ 中缺少 "placement delete",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43199545/

相关文章:

c++ - 使用 C++ 操作 protobuf 中的数据结构

c++ - Jenkins 和单元测试框架支持的 C 插件?

c++ - 我如何继承 mpl::vector 中的所有类型?

python - Pickle 输掉了我的一个论点

c++ - 需要有关 Qt 代码块配置的帮助!

c++ - 函数返回时 Linux c++ system() 调用崩溃

c++ - 关于 C++ 中 new 的放置

constructor - 使用构造函数参数列表而不是参数本身调用 Java new(在 Clojure 中)

c++ - 何时调用删除运算符(operator)?

c# - 多重继承和 C#