我遇到的情况是,同时访问的容器的分配器需要调用placement 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)
}
};
答案 0 :(得分:6)
鉴于破坏意味着对象生命周期的结束,处理双重破坏是为了获得没有格式良好的代码应该遇到的好处而付出代价(将内存留在可重新构建状态的额外工作)。
说双重破坏是可以的,相当于说使用后免费是好的;当然,在特定情况下允许它的分配器可能会使错误代码保持正常工作,但这并不意味着如果它阻止分配器在非病理情况下有效且正确地工作,那么这是一个值得在分配器中保留的功能。
如果您处于可能出现双重破坏的位置,那么您可能处于可以进行破坏后使用的位置,现在您必须对您的班级进行每项操作检查并且“处理”(无论这意味着)在被破坏的实例上被调用的可能性。你已经妥协了正常的操作以允许滥用,这是一条可怕的开始道路。
简短版:不要防止双重破坏;在你被双重破坏的那一刻,有问题的代码无论如何都进入了未定义的行为区域,并且处理它不是你的工作。编写一开始就没有做过可怕事情的代码。
答案 1 :(得分:2)
嗯,实际上不可能这样做。或者至少,不是没有重大的痛苦。
在类的析构函数执行完毕后,看到它的所有子对象都停止存在。因此,当析构函数的第二次调用尝试访问其任何子对象时,您将调用UB。
因此,如果您要进行双重破坏安全,则需要在类外部存储状态,以便能够判断特定实例是否已被销毁。因为你可以拥有任意数量的类实例,所以处理这个问题的唯一方法就是让构造函数分配一些内存并将this
指针注册到某个东西,析构函数可以检查它。
这必须发生在该对象的每个双重破坏安全子对象的每个实例上。这是巨额的开销,所有这些都是为了阻止不应该发生的事情。
正如雷蒙德·陈指出的那样,只有在任何非平凡可破坏类型上调用双重破坏的行为都是未定义的行为。
[basic.life] / 1告诉我们,在调用析构函数时,具有非平凡析构函数的对象的生命周期结束。 [basic.life] / 6告诉我们:
在对象的生命周期开始之前但在对象将占用的存储空间被分配之后,或者在对象的生命周期结束之后以及在重用或释放对象占用的存储之前,表示任何指针可以使用对象将位于或位于的存储位置的地址,但仅限于有限的方式。 [...]允许通过这样的指针进行间接,但是得到的左值只能以有限的方式使用,如下所述。如果出现以下情况,该程序具有未定义的行为:
...
指针用于访问非静态数据成员或调用对象的非静态成员函数,或
析构函数是“对象的非静态成员函数”。所以事实上,不可能使C ++类型的双重破坏安全。
答案 2 :(得分:0)
不,你不应该试图防止滥用代码。 C ++的强大功能可以使这样的滥用成为可能,但您必须信任(并记录)遵守预期用途。
我们是否会在所有桥梁周围设置12英尺高的围栏以阻止和保护跳线(成本高昂),或者我们应该使用高效且正常的护栏并相信每个人都会遵守预期和合理的用例?