In the following code, a wrapper<T>
object is declared which contains a movable<T>
, where T
is an incomplete type. The destructor of movable
is made so that it cannot be instantiated without complete knowledge of T
, but wrapper
's destructor is only forward-declared, which means that it should be sufficient if ~movable()
is instantiated at the point of definition of ~wrapper()
.
#include <utility>
template<class T>
struct movable {
movable() noexcept = default;
~movable() noexcept { (void) sizeof(T); }
movable(const movable&) noexcept = delete;
movable(movable &&) noexcept = default;
};
template<class T>
class wrapper {
public:
movable<T> m;
wrapper() noexcept = default;
wrapper(wrapper &&) noexcept = default;
~wrapper();
};
struct incomplete;
int main() {
/* extern */ wrapper<incomplete> original;
wrapper<incomplete> copy(std::move(original));
}
(Try it here)
However, wrapper()
wants to instantiate ~movable()
. I get that in case of an exception, destruction of members must be possible, but movable()
and wrapper()
are both noexcept. Interestingly, the move constructor works fine (try uncommenting the extern
part in the example code.)
What is the reason for this behaviour, and is there a way to circumvent it?
As observed by T.C.,
In a non-delegating constructor, the destructor for [...] each non-static data member of class type is potentially invoked [...]
Per DR1424, the motivation is to make it clear that an implementation is required to issue an error if a destructor is inaccessible from the constructor of the parent object, "[even if] there is no possibility for an exception to be thrown following a given sub-object's construction".
The destructor of movable<T>
is accessible, but it cannot be instantiated, which is where your problem arises as a potentially invoked destructor is odr-used.
This makes life simpler for the implementor, as they can just verify that each subobject has an accessible and if necessary instantiable destructor, and leave it to the optimizer to eliminate destructor calls that are not required. The alternative would be horribly complicated - a destructor would be required or not required depending on whether any succeeding subobjects were noexcept constructible, and on the constructor body.
The only way to avoid potential invocation of the destructor would be to use placement new, taking over management of the lifetime of the subobject yourself:
#include <new>
// ...
template<class T>
class wrapper {
public:
std::aligned_storage_t<sizeof(movable<T>), alignof(movable<T>)> m;
wrapper() noexcept { new (&m) movable<T>; };
wrapper(wrapper&& rhs) noexcept { new (&m) movable<T>{reinterpret_cast<movable<T>&&>(rhs.m)}; }
~wrapper();
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With