我正在开发一个旧的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-不完整
嗯,是的,很明显。但由于这些对象总是从池中分配的,我认为没有必要使用自定义(非放置) operator delete
。所以我试着删除D2行。但这会导致编译失败:
new.cc: In析构函数‘virtual B::~B()’:new.cc:9:8:错误:没有合适的‘operator delete’用于‘B’结构B:a^ new.cc: At global作用域: new.cc:18:31:注意:合成方法‘virtual B::~B()’首先需要在这里B *doit1() { return new(p) B;}
一些研究表明问题出在B的虚拟析构函数上。它需要调用非放置B::operator delete
,因为某个地方的某个人可能会试图通过B
来delete
一个B
。多亏了名称隐藏,行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::operator delete’in‘struct A’”)。
相关但不同的问题:
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
不一定是正确的事情。
如果你认为你有一个很好的论点,即我对应该或不应该工作的期望是错误的,我也很想听到这一点。
发布于 2016-09-06 05:20:04
有趣的问题。如果我理解正确的话,您要做的是根据是否通过池分配来选择正确的delete操作符。
您可以在池中已分配数据块的开头存储一些有关该数据块的额外信息。
由于B不能在没有池的情况下被分配,你只需要使用关于池的额外信息转发到普通删除(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;
}
发布于 2016-12-21 07:34:52
这真的很难理解你要用这段代码来实现什么,因为你去掉了它的重要部分。
你知道吗,只有当B的构造函数抛出异常时,才会调用static void operator delete(void *m, Pool &p) { }
?
15)如果已定义,则在对象的构造函数抛出异常时,由具有匹配签名的自定义单对象放置新表达式调用。如果定义了特定于类的版本(25),则优先调用它而不是(9)。如果用户既没有提供(25)也没有提供(15),则不调用解除分配函数。
这意味着在当前示例中,该操作符delete (D1)将永远不会被调用。
对我来说,有一个带有虚拟析构函数的基类A,并坚持删除调用的语义是不同的,这看起来很奇怪,这取决于对象的创建方式。
如果你真的需要基类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
};
https://stackoverflow.com/questions/39336353
复制相似问题