【问题标题】:C++ destruction order: Calling a field destructor before the class destructorC++ 销毁顺序:在类析构函数之前调用字段析构函数
【发布时间】:2018-01-02 00:44:50
【问题描述】:

有没有办法在类析构函数之前调用字段析构函数?

假设我有 2 个类 SmallBig,并且 Big 包含 Small 的实例作为其字段:

class Small
{
public:
    ~Small() {std::cout << "Small destructor" << std::endl;}
};

class Big
{
public:
    ~Big() {std::cout << "Big destructor" << std::endl;}

private:
    Small small;
};

int main()
{
    Big big;
}

这当然是在小析构函数之前调用大析构函数:

Big destructor
Small destructor

我需要在Big 析构函数之前调用Small 析构函数,因为它为Big 析构函数做了一些必要的清理工作。

我可以:

  1. 显式调用small.~Small() 析构函数。 -> 然而,这会调用Small 析构函数两次:一次是显式调用,一次是在Big 析构函数执行后。
  2. 有一个Small* 作为字段并在Big 析构函数中调用delete small;

我知道我可以在 Small 类中有一个函数来进行清理并在 Big 析构函数中调用它,但我想知道是否有办法反转析构函数的顺序。

有没有更好的方法来做到这一点?

【问题讨论】:

  • 我认为没有更好的方法,因为 Small 析构函数只有在 Big 被销毁时才会被调用,因此,在 Big 析构函数之后。
  • 在 big 的析构函数之前调用 small 的析构函数需要什么样的清理?
  • “我需要在大析构函数之前调用小析构函数,因为它为大析构函数做了一些必要的清理工作。”你的设计坏了。问如何修复它,不要问如何忍受损坏的设计。
  • “我需要在大析构函数之前调用小析构函数” - 这听起来像 XY problem - 为什么需要这样做?
  • @nefas 是用来销毁zmq上下文的,小析构函数会关闭一些socket。大的析构函数破坏了zmq上下文,需要关闭所有的socket才能被销毁。

标签: c++ destructor object-destruction


【解决方案1】:

在不知道为什么你想这样做的情况下,我唯一的建议是将Big 分解为需要在Small 之后销毁的部分,然后使用组合来包含里面Big。然后你就可以控制销毁的顺序了:

class Small
{
public:
    ~Small() {std::cout << "Small destructor" << std::endl;}
};

class BigImpl
{
public:
     ~BigImpl() { std::cout << "Big destructor" << std::endl; }
};

class Big
{
private:
    BigImpl bigimpl;
    Small small;
};

【讨论】:

    【解决方案2】:

    显式调用 small.~Small() 析构函数。 -> 然而,这会调用小析构函数两次:一次是显式调用,另一次是在大析构函数执行后。

    好吧,我不知道您为什么要继续使用这种有缺陷的设计,但是您可以使用placement new 解决您第一个项目符号中描述的问题
    它遵循一个最小的工作示例:

    #include <iostream>
    
    struct Small {
        ~Small() {std::cout << "Small destructor" << std::endl;}
    };
    
    struct Big {
        Big() { ::new (storage) Small; }
    
        ~Big() {
            reinterpret_cast<Small *>(storage)->~Small();
            std::cout << "Big destructor" << std::endl;
        }
    
        Small & small() {
            return *reinterpret_cast<Small *>(storage);
        }
    
    private:
        unsigned char storage[sizeof(Small)];
    };
    
    int main() {
        Big big;
    }
    

    您不再有 Small 类型的变量,但使用示例中的 small 成员函数之类的东西,您可以轻松解决它。

    这个想法是你保留足够的空间来就地构造一个Small,然后你可以像你一样显式地调用它的析构函数。它不会被调用两次,因为 Big 类必须释放的是 unsigned chars 的数组。
    此外,您不会将Small 直接存储到动态存储中,因为实际上您正在使用Big 的数据成员来创建它。


    话虽如此,我建议您在动态存储上分配它,除非您有充分的理由不这样做。使用std::unique_ptr 并将其重置在Big 的析构函数的开头。您的Small 将在析构函数的主体按预期实际执行之前消失,并且在这种情况下析构函数不会被调用两次。


    编辑

    正如 cmets 中所建议的,std::optional 可以是另一个可行的解决方案,而不是 std::unique_ptr。请记住,std::optional 是 C++17 的一部分,因此您是否可以使用它主要取决于您必须遵守的标准的修订版本。

    【讨论】:

    • 是的,我同意动态存储。这就是我在'2'中提到的。我现在正在使用这种方法。
    • 我想知道std::optional 是否比unique_ptr 更可取。
    • @ChrisDrew Small 据我了解不是可选的。可选的语义在这里不太合适。我的两分钱。
    • 就个人而言,我认为std::optional 的语义与可为空的智能指针一样适合,并且具有不使用动态内存的好处。
    • 一般来说,storage 可能无法正确对齐 Small。你应该使用std::aligned_storage
    【解决方案3】:

    析构函数调用的顺序不能改变。设计它的正确方法是Small 执行自己的清理。

    如果您无法更改Small,那么您可以创建一个包含Small 的类SmallWrapper,并且还可以执行所需的清理。

    标准容器 std::optionalstd::unique_ptrstd::shared_ptr 可能足以满足此目的。

    【讨论】:

      猜你喜欢
      • 2018-05-02
      • 2014-05-12
      • 2016-11-10
      • 2018-05-08
      • 1970-01-01
      • 1970-01-01
      • 2012-04-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多