在 C++ 中强制执行成员销毁顺序编译时

Ale*_*998 6 c++ destructor

我有一个A像这样的 C++ 类:

\n
// third-party classes / methods, unable to change\nclass C {};\nclass B {\npublic:\n    C* getC();\n};\nB* legacyAPIToGetB(const char*);\n\n// the target class\nclass A {\npublic:\n    A() {\n        // not using initialization lists\n        // because something have to be calculated prior B construction\n        b = std::unique_ptr<B>(legacyAPIToGetB("calculated value"));\n        // throw an exception (don\'t construct A) if B cannot be constructed\n        if (!b) throw std::exception{"Cannot get B"};\n\n        c = std::unique_ptr<C>(b->getC());\n        if (!c) throw std::exception{"Cannot get C"};\n    }\n\n    std::unique_ptr<B> b = nullptr;\n    std::unique_ptr<C> c = nullptr;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

Sayb是一个动态库句柄,并且c是使用该库创建的对象。\n在这种情况下,b在 后调用 的析构函数c,这很好。

\n

然而,如果有人后来重新组织代码,他们可能会意外地交换b和的c声明。\n因此破坏顺序被颠倒,导致可能难以诊断的运行时崩溃。

\n

我想确保如果有人意外地进行了更改,代码应该无法编译(可能带有自定义错误消息)。这可能吗?

\n

(或者这个设计从一开始就有缺陷?)

\n
\n

我尝试使用static_assert来确保会员订单:

\n
static_assert(\n    reinterpret_cast<void*>(&reinterpret_cast<A*>(nullptr)->b)\n    < reinterpret_cast<void*>(&reinterpret_cast<A*>(nullptr)->c),\n    "destruction of b should be after c");\n
Run Code Online (Sandbox Code Playgroud)\n

然而,这似乎是无效的 C++ 代码,只能在 MSVC 中运行。

\n

更新:可以使用offsetof宏来检查成员的布局,请参阅IlCapitano\'s 答案了解详细信息。\n虽然“如果 type 不是标准布局类型(自 C++11 起),则 offsetof 的结果是未定义的(直到C++17) / offsetof 宏的使用是有条件支持的 (C++17 起)”,看来主要编译器至少在这种情况下支持它。

\n

(我什至不确定断言内存布局是否保证了销毁顺序!)
\n更新:不幸的是,在 C++23 之前,不能保证稍后声明的成员将具有更高的地址。

\n
\n

(C++23 之前):由访问说明符分隔的成员(C++11 之前)/具有不同访问控制的成员(C++11 起)以未指定的顺序分配(编译器可能将它们分组在一起)。来源

\n
\n
\n

正如评论中所述,可以按reset正确的顺序使用指向指针的自定义析构函数:

\n
A::~A() {\n    // destruction of b should be after c\n    c.reset();\n    b.reset();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

但是,如果构造函数抛出异常,则对象\xe2\x80\x99s 析构函数不会运行A::A()。\n如上所述,如果出现问题,我将抛出异常。这也是我使用s的一个原因unique_ptr

\n

可以使用一些技巧(例如,将其他值保留在本地,然后将std::swap它们一次性放入类中)来实现异常安全,但是,这会增加复杂性。

\n

MSa*_*ers 4

这看起来像是一个所有权被人为简化的案例。既然c需要b活着,那么拥有c一个shared_ptr<B>. 该所有权将与 共享class A

交换A::bA::c会导致A::c提前重置,但C库仍保持加载状态,因为b仍然有一个指针。

  • @AlexGuo1998:你总是可以包装`B`和/或`C`。特别是,这里应该很容易组合 `C` 和 `std::shared_ptr&lt;B&gt;`。 (3认同)