为什么一个noexcept构造函数需要实例化析构函数?

10
在以下代码中,声明了一个包含一个可移动对象movable<T>wrapper<T>对象,其中T是不完整类型。 movable的析构函数被设计成不能在没有完整了解T的情况下实例化,但是wrapper的析构函数只是前向声明,这意味着在定义~wrapper()时实例化~movable()应该是足够的。请注意保留HTML标签。
#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() 都是 noexcept 的。有趣的是,移动构造函数可以正常工作(请尝试取消注释示例代码中的 extern 部分)。

这种行为的原因是什么,有没有办法规避它?


1
即使取消注释extern,clang编译仍然失败。 - interjay
嗯,“如果类类型的完整性可能影响程序的语义,那么将实例化一个类模板”……也许这与此有关? - Kerrek SB
如果一个类有多个成员,在构造期间其中一个初始化抛出异常,那么之前完成的初始化必须通过调用相应成员的析构函数来撤销。我认为你看到的与此有关。 - dyp
请注意,这两个类都是可以平凡移动构造的,但不能平凡默认构造。movable 的非平凡析构函数会影响默认构造函数的平凡性,但不会影响移动构造函数的平凡性(出人意料)。 - dyp
3
在非委托的构造函数中,可能构造的每个类类型子对象的析构函数都可能被调用。 - T.C.
1个回答

4

根据T.C.的观察,

在非委托构造函数中,类类型的每个非静态数据成员的析构函数都有可能被调用。

根据DR1424,这么做是为了明确表明实现必须发出错误提示,如果一个析构函数从父对象的构造函数中不可访问,"[即使]在给定子对象构造后没有抛出任何异常的情况下"也要这样做。

movable<T>的析构函数是可以访问的,但它无法被实例化,这就是您遇到的问题,因为可能会调用的析构函数会被odr-used

这对于实现者来说更加简单,因为他们只需要验证每个子对象是否具有可访问并且必要时可实例化的析构函数,并将消除不需要的析构函数调用交给优化器。否则将非常复杂 - 析构函数的需求或不需求取决于是否存在后续子对象是noexcept可构造的以及构造函数体。

唯一避免潜在调用析构函数的方法是使用放置new,接管子对象的生命周期管理:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接