类是否应该能够抵御双重破坏
Should classes be resilient to double destroy?
我遇到一种情况,即并发访问容器的分配器需要同时调用放置 new 和析构函数。
template< class U > void destroy(U* p){
p->~U();
}
事实上,我最终可以反复调用破坏。 这让我想到这样的事情是否应该可以。
std::vector<int>* p = (std::vector<int>*)malloc(sizeof(std::vector<int>));
::new (*p) std::vector<int>(30);
(*p)[10] = 5;
p->~std::vector<int>();
p->~std::vector<int>();
free(p);
我认为只要销毁std::vector
将数据指针设置为 null 或 size 设置为零,并且再次调用时没有双重释放,这将起作用。
那么,是否应该将类设置为意外(或良性)双重销毁应等同于单次销毁?
换句话说,销毁是否应该是一种幂等操作?
(请注意,为简单起见,此示例中的析构函数不是虚拟的)
我发现这个问题类似于如何允许稍后销毁移动类的问题。
一些答案将运行时成本用于反对支持双重销毁的理由。 有成本,但它们类似于移动物体的成本。 换句话说,如果移动便宜,那么允许双重破坏是便宜的(如果DD首先不是UB,因为其他原因,例如标准)。
具体来说:
class dummyvector{
int* ptr;
public:
dummyvector() : ptr(new int[10]){}
dummyvector(dummyvector const& other) = delete; // for simplicity
dummyvector(dummyvector&& other) : ptr(other.ptr){
other.ptr = nullptr; // this makes the moved object unusable (e.g. set) but still destructable
}
dummyvector& operator=(dummyvector const&) = delete; // for simplicity
void set(int val){for(int i = 0; i != 10; ++i) ptr[i]=val;}
int sum(){int ret = 0; for(int i = 0; i != 10; ++i) ret += ptr[i]; return ret;}
~dummyvector(){
delete[] ptr;
ptr = nullptr; // this is the only extra cost for allowing double free (if it was not UB)
// ^^ this line commented would be fine in general but not for double free (not even in principle)
}
};
鉴于销毁意味着对象的生命周期结束,处理双重销毁是付出代价(使内存处于可重新破坏状态的额外工作),以获得任何格式良好的代码都不应该遇到的好处。
说双重销毁是可以的,相当于说释放后使用是好的;当然,在特定情况下允许它的分配器可能会保持错误的代码工作,但这并不意味着如果它阻止分配器在非病理情况下高效和正确地工作,那么它就是值得在分配器中保留的功能。
如果你曾经处于双重销毁的可能性,那么你可能处于一个可以销毁后使用的位置,现在你必须对你的类的每个操作进行检查和"处理"(无论这意味着什么)在销毁实例上被调用的可能性。您已经损害了正常操作以允许滥用,这是一条可怕的开始之路。
简短版本:不要防止双重破坏;当你被双重销毁的那一刻,有问题的代码无论如何都进入了未定义的行为领域,处理它不是你的工作。编写一开始就不会做可怕事情的代码。
好吧,实际上不可能做到这一点。或者至少,并非没有明显的痛苦。
请参阅,在类的析构函数完成执行后,其所有子对象都不再存在。因此,当析构函数的第二次调用尝试访问其任何子对象时,您将调用 UB。
因此,如果要实现双重销毁安全,则需要在类之外存储状态,以便能够判断特定实例是否已被销毁。由于你可以有一个类的任意数量的实例,处理这个问题的唯一方法是让构造函数分配一些内存,并使用析构函数可以检查的内容注册this
指针。
对于该对象的每个双重破坏安全子对象的每个实例,都必须发生这种情况。这是大量的开销,所有这些都是为了阻止不应该发生的事情。
正如Raymond Chen所指出的,仅仅对任何非平凡可破坏的类型调用双重破坏的行为都是未定义的行为。
[basic.life]/1 告诉我们,具有非平凡析构函数的对象在调用析构函数时结束其生存期。[basic.life]/6告诉我们:
在对象的生存期开始之前,但在分配了对象将占用的存储之后,或者在对象的生存期结束后,在重新使用或释放对象占用的存储之前,可以使用表示对象将位于或曾经所在的存储位置地址的任何指针,但只能以有限的方式使用。[...]允许通过此类指针进行间接寻址,但生成的左值只能以有限的方式使用,如下所述。在以下情况下,程序具有未定义的行为:
。
指针用于访问非静态数据成员或调用对象的非静态成员函数,或
析构函数是"对象的非静态成员函数"。所以事实上,不可能让C++型双重破坏安全。
不,您不应该试图防止滥用代码。C++的力量使这样的滥用成为可能,但你必须相信(并记录)预期的用途是被遵守的。
我们是要在所有桥梁周围竖起 12 英尺高的围栏来阻止和保护跳线者(以高昂的成本),还是应该只使用高效和正常的护栏并相信每个人都会遵守预期和合理的用例?
- 在提升multi_index容器中,是否定义了"default index"?
- 在C++STL中是否有Polyval(Matlab函数)等价物?
- 检查输入是否不是整数或数字
- 是否可以初始化不可复制类型的成员变量(或基类)
- 在C++中,是否可以基于给定的标识符创建基类的新实例,反之亦然
- 是否可以通过C++扩展强制多个python进程共享同一内存
- 此代码是否违反一个定义规则
- 是否需要删除包含对象的"pair"?
- 是否可以从int转换为enum类类型
- 无论条件是否为true,if总是在c++中执行
- 如何找到大小'x'数组是否完全填充,在C++?
- 检查值是否在集合p1和p2中,但不在p3中
- 是否可以在编译时初始化数组,以便在运行时不会花费时间?
- 检查 std::shared_ptr<> 的当前底层类型是否为 T
- 在c++中检查长方体是否尽可能快地重叠(无迭代)
- GL_SHADERSTORAGE_BUFFER位置是否与其他着色器位置冲突
- 子目录是否继承属性,例如add_definitions,include_directories和父Cmakelist.t
- 标准是否使用多余的大括号(例如 T{{{10}}})定义列表初始化?
- C/C++预处理器是否可以检测一些编译器选项
- 是否可以用"iostream"包装现有的TCP/OOpenSSL会话