I have a C++ class A
like this:
// third-party classes / methods, unable to change
class C {};
class B {
public:
C* getC();
};
B* legacyAPIToGetB(const char*);
// the target class
class A {
public:
A() {
// not using initialization lists
// because something have to be calculated prior B construction
b = std::unique_ptr<B>(legacyAPIToGetB("calculated value"));
// throw an exception (don't construct A) if B cannot be constructed
if (!b) throw std::exception{"Cannot get B"};
c = std::unique_ptr<C>(b->getC());
if (!c) throw std::exception{"Cannot get C"};
}
std::unique_ptr<B> b = nullptr;
std::unique_ptr<C> c = nullptr;
};
Say b
is a dynamic library handle, and c
is an object created with that library.
In this case, destructor of b
is called after c
, which is fine.
However if someone later reorganizes the code, they could accidentally swap the declaration of b
and c
.
Thus the destruction order is reversed, leading to possibly hard-to-diagnose runtime crash.
I want to make sure if someone accidentally makes that change, the code should fail to compile (possibly with a custom error message). Is this possible?
(Or maybe this design is flawed from the beginning?)
I tried to use static_assert
to ensure a member order:
static_assert(
reinterpret_cast<void*>(&reinterpret_cast<A*>(nullptr)->b)
< reinterpret_cast<void*>(&reinterpret_cast<A*>(nullptr)->c),
"destruction of b should be after c");
However, this seems invalid C++ code which only works in MSVC.
Update: One may use the offsetof
macro to check the layout of members, see IlCapitano's answer for detail.
While "if type is not a standard layout type (since C++11), the result of offsetof is undefined (until C++17) / use of the offsetof macro is conditionally-supported (since C++17)", it seems major compilers supported it at least in this situation.
(I'm not even sure whether asserting the memory layout guarantees the destruction order!)
Update: Unfortunately, it's not guaranteed that the members declared later will have higher addresses, until C++23.
(until C++23): Members separated by an access specifier (until C++11) / with different access control (since C++11) are allocated in unspecified order (the compiler may group them together). source
As noted in the comment, it's possible to use a custom destructor to reset
the pointers in the correct order:
A::~A() {
// destruction of b should be after c
c.reset();
b.reset();
}
However if a constructor throws an exception, the object’s destructor is not run.
As noted above, I'm throwing an exception from A::A()
if something goes wrong. This is also a reason why I use unique_ptr
s.
It's possible to use some tricks (e.g. keep the other values local, and std::swap
them into the class in one go) to achieve exception safety, however, this is added complexity.
release
the objects in the correct order? Then the order of destruction is irrelevant. Remember to add documentation and comments to say why this is needed. – Elbringunique_ptr
for automaticdelete
-ing. Sure we can use other tricks to achieve exception safety, but that's added complexity. – Mewstd::unique_ptr<B> b = nullptr;
- this is a bit weird. The default ctor does the same thing. – Jesusitajetstatic_assert
, you might do it withoffsetof
. – Consistency