C++析构顺序:在调用类析构函数之前调用字段析构函数

6
有没有办法在类析构函数之前调用字段析构函数?
假设我有两个类Small和Big,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析构函数中调用它,但我想知道是否有一种方式来颠倒析构函数的顺序。
有更好的方法吗?

6
在小对象的析构函数在大对象的析构函数之前被调用的情况下,需要进行何种清理工作? - nefas
12
我需要先调用小析构函数,因为它执行了一些大析构函数所必需的清理工作。你的设计有问题,应该询问如何修复它,而不是如何忍受一个有问题的设计。 - n. m.
4
"我需要在大的析构函数被调用之前调用小的析构函数" - 这听起来像是一个XY问题 - 为什么你需要这样做? - Oliver Charlesworth
我建议重新设计,而不是试图规避构造/析构的标准顺序。 - NathanOliver
1
@nefas 这是用于销毁zmq上下文的,小型析构函数会关闭一些套接字。大型析构函数会销毁zmq上下文,并需要在销毁之前关闭所有套接字。 - CK.
显示剩余5条评论
3个回答

2

如果不知道你为什么要这样做,我的建议是将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() 析构函数。然而,这会导致 small 析构函数被调用两次:一次是显式调用,一次是在 big 析构函数执行后自动调用。
好吧,我不知道为什么你想坚持这个有缺陷的设计,但你可以使用放置 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 char数组,因此它不会被调用两次。
此外,您不会直接将Small存储到动态存储中,因为实际上您正在使用Big的数据成员在其中创建它。
话虽如此,除非您有充分的理由不这样做,否则我建议您将其分配到动态存储中。使用std::unique_ptr并在Big的析构函数开始时重置它。您的Small将在预期之前消失,并且在这种情况下析构函数不会被调用两次。
编辑 正如评论中提出的建议,std::optional也可以是另一种可行的解决方案,而不是std::unique_ptr。请记住,std::optional是C++17的一部分,因此是否可以使用它主要取决于您必须遵循哪个标准版本。

是的,我同意动态存储。这就是我在“2”中提到的。目前我正在使用这种方法。 - CK.
1
我在想,使用std::optional是否比unique_ptr更好。 - Chris Drew
@ChrisDrew,就我理解而言,“Small”不是可选的。这里可选项的语义不太适用。以上仅供参考。 - skypjack
就我个人而言,我认为 std::optional 的语义与可空智能指针一样适合,并且具有不使用动态内存的好处。 - Chris Drew
2
通常情况下,“存储”可能对“Small”没有正确对齐。您应该使用“std::aligned_storage”。 - M.M

0

析构函数的调用顺序是无法改变的。正确的设计方式是让Small执行自己的清理工作。

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

标准容器std::optionalstd::unique_ptrstd::shared_ptr可能适用于此目的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接