我有一个A像这样的 C++ 类:
// 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};\nRun Code Online (Sandbox Code Playgroud)\nSayb是一个动态库句柄,并且c是使用该库创建的对象。\n在这种情况下,b在 后调用 的析构函数c,这很好。
然而,如果有人后来重新组织代码,他们可能会意外地交换b和的c声明。\n因此破坏顺序被颠倒,导致可能难以诊断的运行时崩溃。
我想确保如果有人意外地进行了更改,代码应该无法编译(可能带有自定义错误消息)。这可能吗?
\n(或者这个设计从一开始就有缺陷?)
\n我尝试使用static_assert来确保会员订单:
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");\nRun Code Online (Sandbox Code Playgroud)\n然而,这似乎是无效的 C++ 代码,只能在 MSVC 中运行。
\n更新:可以使用offsetof宏来检查成员的布局,请参阅IlCapitano\'s 答案了解详细信息。\n虽然“如果 type 不是标准布局类型(自 C++11 起),则 offsetof 的结果是未定义的(直到C++17) / offsetof 宏的使用是有条件支持的 (C++17 起)”,看来主要编译器至少在这种情况下支持它。
(我什至不确定断言内存布局是否保证了销毁顺序!)
\n更新:不幸的是,在 C++23 之前,不能保证稍后声明的成员将具有更高的地址。
\n\n(C++23 之前):由访问说明符分隔的成员(C++11 之前)/具有不同访问控制的成员(C++11 起)以未指定的顺序分配(编译器可能将它们分组在一起)。来源
\n
正如评论中所述,可以按reset正确的顺序使用指向指针的自定义析构函数:
A::~A() {\n // destruction of b should be after c\n c.reset();\n b.reset();\n}\nRun Code Online (Sandbox Code Playgroud)\n但是,如果构造函数抛出异常,则对象\xe2\x80\x99s 析构函数不会运行A::A()。\n如上所述,如果出现问题,我将抛出异常。这也是我使用s的一个原因unique_ptr。
可以使用一些技巧(例如,将其他值保留在本地,然后将std::swap它们一次性放入类中)来实现异常安全,但是,这会增加复杂性。
这看起来像是一个所有权被人为简化的案例。既然c需要b活着,那么拥有c一个shared_ptr<B>. 该所有权将与 共享class A。
交换A::b并A::c会导致A::c提前重置,但C库仍保持加载状态,因为b仍然有一个指针。