具有虚拟析构函数的池分配器

Nem*_*emo 16 c++ language-lawyer

我正在开发一个旧的C++ 03代码库.一节看起来像这样:

#include <cstddef>

struct Pool
{ char buf[256]; };

struct A
{ virtual ~A() { } };

struct B : A
{
  static void *operator new(std::size_t s, Pool &p) { return &p.buf[0]; }
  static void operator delete(void *m, Pool &p) { } // Line D1
  static void operator delete(void *m) { delete m; } // Line D2
};

Pool p;

B *doit() { return new(p) B; }
Run Code Online (Sandbox Code Playgroud)

也就是说,B派生自A,但B的实例是从内存池中分配的.

(请注意,此示例稍微过于简单......实际上,池分配器执行的操作非常重要,因此operator delete需要在第D1行放置.)

最近,我们在更多编译器上启用了更多警告,而D2行引出了以下警告:

警告:删除'void*'未定义[-Wdelete-incomplete]

嗯,是的,很明显.但由于这些对象总是从池中分配,我认为不需要自定义(非放置)operator delete.所以我尝试删除D2线.但这导致编译失败:

new.cc:在析构函数'virtual B :: ~B()':new.cc:9:8:错误:没有合适的'operator delete'为'B'结构B:A ^ new.cc:在全局范围: new.cc:18:31:注意:合成方法'virtual B :: ~B()'首先要求B*doit1(){return new(p)B; }

一点研究确定问题是B的虚拟析构函数.它需要调用非配置B::operator delete,因为有人的地方可能会尝试delete一个B通过A *.由于名称隐藏,第D1行呈现默认的非展示位置operator delete无法访问.

我的问题是:处理这个问题的最佳方法是什么?一个明显的解决方

static void operator delete(void *m) { std::terminate(); } // Line D2
Run Code Online (Sandbox Code Playgroud)

但这感觉不对......我的意思是,我是谁坚持你必须从池中分配这些东西?

另一个明显的解决方案(以及我目前使用的):

static void operator delete(void *m) { ::operator delete(m); } // Line D2
Run Code Online (Sandbox Code Playgroud)

但这也错了,因为我怎么知道我正在调用正确的删除功能?

我认为,我真正想要的是using A::operator delete;,但是没有编译("没有成员匹配'结构A'中的'A :: operator delete'').

相关但不同的问题:

为什么虚拟析构函数需要删除运算符

没有删除任何功能时,Clang会抱怨"无法覆盖已删除的功能"

[更新,扩大一点]

我忘了提到析构函数A并不需要virtual在我们当前的应用程序中.但是从具有非虚拟析构函数的类派生会导致一些编译器在您提高警告级别时抱怨,并且练习的原始点是消除此类警告.

此外,只是要明确所需的行为......正常的用例如下所示:

Pool p;
B *b = new (p) B;
...
b->~B();
// worry about the pool later
Run Code Online (Sandbox Code Playgroud)

也就是说,就像大多数使用placement new一样,你直接调用析构函数.或者调用辅助函数为您完成.

希望以下工作; 事实上,我认为这是一个错误:

Pool p;
A *b_upcast = new (p) B;
delete b_upcast;
Run Code Online (Sandbox Code Playgroud)

检测和失败这种错误的使用将是正常的,但只有在不增加任何开销给非错误情况的情况下才能完成.(我怀疑这是不可能的.)

最后,我希望这个工作:

A *b_upcast = new B;
delete b_upcast;
Run Code Online (Sandbox Code Playgroud)

换句话说,我想支持但不要求为这些对象使用池分配器.

我目前的解决方案大多有效,但我担心直接调用::operator delete不一定是正确的.

如果你认为你有一个很好的论据,我对应该或不应该工作的期望是错误的,我也想听到这个.

pok*_*909 1

有趣的问题。如果我理解正确的话,您要做的就是根据是否通过池分配来选择正确的删除运算符。

您可以在池中分配的块的开头存储一些有关该信息的额外信息。

由于无法在没有池的情况下分配 B,因此您只需使用有关池的一些额外信息转发到正常的 delete(void*) 运算符内的放置删除器即可。

运算符 new 会将该部分存储在分配块的开头。

更新: 感谢您的澄清。经过一些小的修改,同样的技巧仍然有效。下面更新了代码。如果这仍然不是您想要做的,那么请提供一些积极和消极的测试用例来定义什么应该起作用,什么不应该起作用。

struct Pool
{
    void* alloc(size_t s) {
        // do the magic... 
        // e.g. 
        //    return buf;
        return buf;
    }
    void dealloc(void* m) {
        // more magic ... 
    }
private:

    char buf[256];
};
struct PoolDescriptor {
    Pool* pool;
};


struct A
{
    virtual ~A() { }
};

struct B : A
{
    static void *operator new(std::size_t s){
        auto desc = static_cast<PoolDescriptor*>(::operator new(sizeof(PoolDescriptor) + s));
        desc->pool = nullptr;
        return desc + 1;
    }

    static void *operator new(std::size_t s, Pool &p){
        auto desc = static_cast<PoolDescriptor*>(p.alloc(sizeof(PoolDescriptor) + s));
        desc->pool = &p;
        return desc + 1;
    }
    static void operator delete(void *m, Pool &p) {
        auto desc = static_cast<PoolDescriptor*>(m) - 1;
        p.dealloc(desc);
    }
    static void operator delete(void *m) {
        auto desc = static_cast<PoolDescriptor*>(m) - 1;
        if (desc->pool != nullptr) {
            throw std::bad_alloc();
        }
        else {
            ::operator delete (desc);
        } // Line D2
    }
};


Pool p;
void shouldFail() { 
    A* a = new(p)B;
    delete a;
}
void shouldWork() { 
    A* a = new B;
    delete a;
}

int main()
{
    shouldWork();
    shouldFail();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)