在以下代码中,声明了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
部分。)
这种行为的原因是什么,有没有办法绕过它?
答案 0 :(得分:4)
正如T.C.所观察到的,
在非委托构造函数中,可能会调用[...]类类型的每个非静态数据成员的析构函数[...]
Per DR1424,其动机是要明确如果从父对象的构造函数中无法访问析构函数,则需要执行一个错误,并且#34; [即使]没有在给定的子对象构造之后抛出异常的可能性&#34;。
movable<T>
的析构函数是可访问的,但它无法实例化,这是您出现问题的地方,因为可能被调用的析构函数 odr-used 。
这使得实现者的生活变得更简单,因为他们可以只验证每个子对象是否具有可访问且必要时可实例化的析构函数,并将其留给优化器以消除不需要的析构函数调用。替代方案将非常复杂 - 根据是否有任何后续子对象是不可构造的,以及在构造函数体上,将需要或不需要析构函数。
避免潜在的析构函数调用的唯一方法是使用placement 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();
};