Почему конструктор noexcept требует создания деструктора?

В следующем коде объявлен объект wrapper<T>, который содержит movable<T>, где T является неполным типом. Деструктор movable создается таким образом, что он не может быть создан без полного знания T, но деструктор wrapper только вперед-декларирован, что означает, что его должно быть достаточно, если ~movable() создается в точке определение ~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));
}

(Попробуйте здесь)

Однако wrapper() хочет создать экземпляр ~movable(). Я получаю это в случае исключения, уничтожение членов должно быть возможным, но movable() и wrapper() оба не исключают. Интересно, что конструктор перемещения работает нормально (попробуйте раскомментировать часть extern в примере кода.)

В чем причина такого поведения, и есть ли способ обойти его?

Ответ 1

Как отмечено T.C.,

В конструкторе без делегирования потенциально вызывается деструктор для [...] каждого нестатического элемента данных типа класса [...]

Per DR1424, мотивация заключается в том, чтобы дать понять, что для выполнения ошибки требуется реализация, если деструктор недоступен из конструктора родительский объект ", [даже если] нет возможности исключить исключение из-за конструкции данного объекта.

Деструктор movable<T> доступен, но он не может быть создан, поэтому ваша проблема возникает, поскольку потенциально вызываемый деструктор используется odr.

Это упрощает жизнь для разработчика, поскольку они могут просто проверить, что каждый подобъект имеет доступный и, если необходимо, реальный деструктор, и оставляйте его оптимизатору для устранения вызовов деструктора, которые не требуются. Альтернатива была бы ужасно сложной - деструктор потребовался бы или не требовался бы в зависимости от того, были ли какие-либо последующие подобъекты конструктивными и не были конструктивными.

Единственный способ избежать потенциального вызова деструктора - это использовать новое размещение, взяв на себя управление временем жизни подобъекта:

#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();
};