1

我听到有人说“C++不需要放置删除,因为它不会执行任何操作。”关于删除表达式在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不同的地址。

注意,当一个使用放置新的[]构造对象的数组,如这里所描述的类似的问题出现了:通过简单地保存数组的大小 Global "placement" delete[]

然而,这可周围没有太多的麻烦工作内存块的头部和处理数组构造函数/析构函数的方法在使用单个对象放置新/显式析构函数调用的循环中手动调用。

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

这里是另一种情况下这将成为一个问题,有丑陋的黑客来解决它:

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

这是不是一个问题实际上更多的只是一个夸夸其谈的为我相当肯定,没有魔术师或平台独立的方法来获取地址存在。在我工作的公司里,我们有一个库,它使用自定义分配器,其上面的黑客工作正常,直到我们必须静态链接到另一个需要替换全局新/删除的程序。我们当前的解决方案是简单地禁止通过指向基础的指针来删除对象,该指针不能显示为始终与最派生的对象具有相同的地址,但这似乎有点不幸。 “ptr->〜T(); free(ptr);”似乎是一种常见的模式,许多人似乎认为它相当于删除表达式,但事实并非如此。我很好奇,如果有其他人遇到了这个问题,他们是如何设法解决这个问题的。

+1

'dynamic_cast '就是这样神奇的操作符。 –

+0

只是一个简短的提示,如果我需要这样的东西,我会使用像scoped_ptr或shared_ptr这样的对象与自定义删除器。这可以用来将对象放回队列或自定义堆,但也可以用于删除/销毁具体类型。 – Sven

+0

在这种情况下,dynamic_cast 几乎就是我正在寻找的。感觉有点不知道这种用法。我意识到智能指针和自定义删除器,不幸的是,所讨论的库的设计方式并不便于使用。 – Anon49343283

回答

2

如果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)); 
} 
+0

哇,你知道的越多......谢谢。 – Anon49343283