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

时间:2016-09-05 19:03:32

标签: 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; }

即,B派生自A,但B的实例是从内存池中分配的。

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

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

  

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

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

  

new.cc:在析构函数中'virtual B :: ~B()':new.cc:9:8:错误:否   适用于'B'结构B的'运算符删除':A            ^ new.cc:在全局范围:new.cc:18:31:注意:合成方法'virtual B :: ~B()'首先需要这里B * doit1(){return   新(p)B; }

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

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

static void operator delete(void *m) { std::terminate(); } // Line D2

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

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

static void operator delete(void *m) { ::operator delete(m); } // Line D2

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

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

相关但截然不同的问题:

Why is delete operator required for virtual destructors

Clang complains "cannot override a deleted function" while no function is deleted

[更新,扩展一下]

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

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

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

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

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

Pool p;
A *b_upcast = new (p) B;
delete b_upcast;

对这种错误的使用进行检测和失败将是正常的,但前提是可以在不对非错误情况增加任何开销的情况下完成。 (我怀疑这是不可能的。)

最后,我希望这可以工作:

A *b_upcast = new B;
delete b_upcast;

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

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

如果你认为你有一个很好的论据,我对应该或不应该做的事情的期望是错误的,我也想听到。

2 个答案:

答案 0 :(得分:1)

有趣的问题。如果我理解正确,你想要做的是选择正确的删除操作符,具体取决于它是否是通过池分配的。

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

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

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

答案 1 :(得分:1)

很难理解你要用这段代码实现什么,因为你剥夺了它的重要部分。

您是否知道只有在B的构造函数抛出异常时才调用static void operator delete(void *m, Pool &p) { }

  

15)如果已定义,则由自定义单对象放置新建   表达式与匹配签名(如果对象的构造函数)   抛出一个例外。如果定义了特定于类的版本(25),则为它   被调用优先于(9)。如果既没有提供(25)也没有提供(15)   用户不会调用释放函数。

这意味着在当前示例中,将永远不会调用此运算符delete(D1)。

对我来说,使用带有虚析构函数的基类A看起来很奇怪,并且坚持认为delete调用的语义不同,具体取决于创建对象的方式。

如果你真的需要基类A,并添加了虚拟析构函数来使警告静音,你可以在A中保护析构函数而不是使其成为虚拟。像这样 -

struct A
{
protected:
  ~A() { }
};

struct B final : public A
{
  ~B() = default;

  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) {} // Line D2

};