类 是否应该对双重破坏有弹性?

Should classes be resilient to double destroy?

我有一种情况,同时访问的容器的分配器需要调用新的放置和析构函数。

    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 或将大小设置为零,并且再次调用时没有双重释放,这就会起作用。

那么,是否应该class将意外(或良性)双重破坏等同于一次破坏?

换句话说,销毁应该是幂等操作吗?

(为简单起见,本例中的析构函数不是虚拟的)

我发现这个问题类似于如何允许稍后销毁移动的 class 的问题。


一些答案将运行时成本作为反对支持双重销毁的理由。 有成本,但它们类似于移动物体的成本。 换句话说,如果移动便宜,允许双重销毁是便宜的(如果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)
   }
};

鉴于销毁意味着对象生命周期的结束,处理双重销毁是要付出代价的(使内存处于可重新销毁状态的额外工作)以获得格式良好的代码永远不会遇到的好处。

说双重销毁没问题等同于说释放后使用没问题;当然,在特定情况下允许它的分配器可能会使有缺陷的代码继续工作,但这并不意味着它是一个值得保留在分配器中的功能,如果它会阻止分配器在非病态情况下有效和正确地工作。

如果您曾经处于可能发生双重破坏的位置,那么您可能处于可能存在破坏后使用的位置,现在您必须对 class 检查并 "handle" (不管那是什么意思)在被销毁的实例上被调用的可能性。您已经损害了正常操作以允许滥用,这是一条糟糕的开始之路。

简短版本:不防止双重破坏;在你被双重破坏的那一刻,有问题的代码无论如何都进入了未定义的行为领域,处理它不是你的工作。编写一开始就不会做糟糕事情的代码。

嗯,这实际上是不可能的。或者至少,并非没有 显着 痛苦。

看,在 class 的析构函数执行完毕后,它的所有子对象都不存在了。因此,当您的析构函数的第二次调用尝试访问其任何子对象时,您将调用 UB。

因此,如果您要实现双重销毁安全,则需要将状态存储在 class 之外,以便能够判断特定实例是否已销毁。并且由于您可以拥有任意数量的 class 实例,处理此问题的唯一方法是让构造函数分配一些内存并将 this 指针注册为析构函数可以检查的内容。

并且对于该对象的每个双重销毁安全子对象的每个实例都必须发生这种情况。这是一个 巨大 的开销,所有这些都是为了阻止不应该发生的事情。


正如 Raymond Chen 指出的那样,仅 act 对任何非平凡可破坏的类型调用双重破坏是未定义的行为。

[basic.life]/1 告诉我们,具有非平凡析构函数的对象在调用析构函数时其生命周期结束。 [basic.life]/6 告诉我们:

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. [...] Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

...

the pointer is used to access a non-static data member or call a non-static member function of the object, or

析构函数是 "non-static member function of the object"。所以实际上,要让一个C++类型的双毁安全是不可能的。

不,您不应该试图防止代码被滥用。 C++ 的强大功能使此类滥用成为可能,但您必须相信(并记录)预期用途得到遵守。

我们是要在我们所有的桥梁周围设置 12 英尺高的围栏以阻止和保护跳线(成本高昂),还是我们应该只使用高效和正常的护栏并相信每个人都会遵守预期的合理用例?