Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a noexcept constructor require instantiation of the destructor?

Tags:

c++

c++11

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?

like image 333
Fabian Knorr Avatar asked Dec 22 '15 15:12

Fabian Knorr


1 Answers

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();
};
like image 158
ecatmur Avatar answered Nov 15 '22 19:11

ecatmur