【问题标题】:Overload operator delete to not delete object - valid or undefined behavior?重载运算符删除以不删除对象 - 有效或未定义的行为?
【发布时间】:2021-06-04 02:49:28
【问题描述】:

前言:我正在优化旧代码库。

我有一个名为 Token 的类,我为它添加了缓存以用于令牌的子集(但不是全部)。缓存的令牌可能不会被删除,因为它们的指针在程序的整个生命周期内都存储在内存中的永久集合中。

不幸的是,代码库中到处都是delete token。所以我所做的是添加一个bool cached 成员,该成员从Token::operator delete 内部检查和析构函数~Token(),如果cached 为真,则立即从这些各自的函数返回。

我的问题是,这是未定义的行为,还是我可以这样做?可以对不会被删除的东西执行delete 运算符吗?或者这会在将来咬我吗?

class Token
{
    bool cached;
    void* data;

public:
    ~Token()
    {
        if (this->cached) return;
        free(data);
    }

    void operator delete(void* p)
    {
        if (((Token*)p)->cached) return;
        ::operator delete(p);
    }

    // operator new, constructor etc.

}

【问题讨论】:

  • 添加您的Token 类的最小示例...
  • ~Token() 内部检查cached 是没有用的,因为到那时该对象已经处于被销毁的过程中。
  • 我添加了一个例子来澄清事情。尽管现在我突然想到,如果该类包含具有自己的析构函数的对象,它们将被销毁,对此我无能为力(这里不是这种情况,但仍然如此)。 @RemyLebeau 如果我没记错的话,析构函数是在运算符删除之前调用的?
  • 那么,真正的目标不是阻止Token对象的销毁,而是它们内部包含的data的销毁?这与您最初提出的问题完全不同。如果是这种情况,您可能根本不需要重载运算符。看到minimal reproducible example 演示令牌的实际使用方式会有所帮助。
  • 不,你误会了;当缓存为真时,我既不想破坏令牌也不想破坏数据。如果将 operator delete 配置为不释放其背后的底层内存,则不会释放令牌。如果析构函数在释放它之前返回,数据也不会。

标签: c++


【解决方案1】:

这不行。 不是 UB 不释放operator delete 中的内存,但它 UB 访问已删除对象内的任何内容。 delete token 将首先调用token 的析构函数,然后调用operator delete。即使你自己在析构函数的主体内部什么都不做,一旦主体返回它会继续销毁对象的所有子对象,一旦整个析构函数返回整个对象就被认为不再存在。即使您的数据成员是bools 或void*s 之类的标量,在销毁时通常不会触及这些实现,该语言仍认为它们无法被析构函数访问。在您的示例中,operator delete 无法访问cached,稍后,如果您仍然有一个指向被破坏对象的“缓存”指针,则使用该指针也是 UB。这就是为什么您在operator delete 中收到void* 而不是Token* 的原因,也是为什么operator delete 隐含为static(不是this):它表示您的对象已经消失了。

如果你在 C++20 领域,你可以使用销毁操作符 delete 代替:

struct Token {
   // ...
   bool cached;
   void operator delete(Token *thiz, std::destroying_delete_t) {
       if(thiz->cached) return;
       thiz->~Token();
       ::operator delete(thiz);
   }
   ~Token() { /* clean up without worrying about cached */ }
};

当存在operator delete 这样的重载时,delete token 将调用重载而不调用析构函数本身,因此您可以安全地选择不销毁对象。当然,现在你有责任自己调用析构函数。

如果你做不到,那你就有点不走运了。您可以尝试void Token::operator delete(void*) = delete;Tokens 上查找delete 的所有用途,以便您可以替换它们。最好用某种智能指针(无论是std::shared_ptr 还是你自己写的东西)替换你的(我认为)Token*s,这样你就不再需要有那么多的deletes。

【讨论】:

  • 很棒的答案,谢谢。我觉得创建一个名为 Delete 的方法可能是值得的,该方法在执行 operator delete 之前检查缓存,然后进行搜索和替换。 C++20 选项看起来不错,如果没有缺点(除了必须手动调用析构函数),那么我可能会先尝试一下。
  • 后续问题:将Token::operator new 中返回的void* 视为Token* 并将缓存的成员设置为true 如果operator newan extra parameter cached真的? (只要构造函数根本不改变成员)
  • @korri123 是的,这也很糟糕。 operator new 在对象构造之前被调用; non-destroying operator delete 在被销毁后被调用。两者都不能访问该对象的任何成员,因为该对象不存在。这是一个void*原因
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-18
  • 2012-07-22
  • 1970-01-01
  • 2012-01-20
  • 2014-09-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多