创建一个不可构造的对象:std::bit_cast是否应该仅应用于具有隐式生存期的类型?
可以设计不可构造的类,但我设法实例化了这样一个类的对象,利用了 std::bit_cast“只在输入和输出时需要平凡可拷贝类型的对象”这一点。
让我们回顾我之前的问题背景 Creating a non constructible object: issue with trivially copyable definition?,我的目标是理解为什么虽然看起来只有一个没有(公开)构造函数的类,仍然不能完全阻止为这个类创建独立对象(非继承的对象)?
理论上可以设计看似不可构造的类(没有公开可用的构造函数),但我还是设法实例化了这样一个类的对象。
我在尝试证明“平凡可拷贝并不意味着隐式生命周期”时偶然遇到了这个问题。[Jarod42提示] 指向一个具有赋值运算符但没有构造函数的类:
#include <bit>
#include <cstring>
#include <iostream>
// Not constructible
// Not implicit lifetime
struct Test {
int val{1};
Test() = delete;
Test(Test const&) = delete;
Test(Test&&) = delete;
Test& operator=(Test const&) = default;
Test& operator=(Test&&) = default;
~Test() = default;
};
int main() {
char storage[sizeof(Test)];
int i{666};
std::memcpy(storage,&i,sizeof(Test));
auto HeWhoMustNotBeNamed = std::bit_cast<Test>(storage);
std::cout << HeWhoMustNotBeNamed.val;
}
所有测试过的编译器(gcc、clang和 msvc)都正确地将 Test 识别为 平凡可拷贝:
- 它具备符合条件的平凡拷贝和移动赋值运算符;
- 没有其他符合条件的特殊成员;
- 并且有一个非删除的平凡构造函数。
Test 和 storage 满足std::bit_cast的前提条件,因此clang和 msvc在没有未定义行为的情况下创建了一个不可构造类的对象。
在我看来,这種行为不应该存在,因为它有悖于类的语义。
(注:gcc会失败,但我猜测这是出于无关的原因:我认为它的内部实现没有进行拷贝消除,因此需要一个移动构造函数,而当前不可用。)
Test 不是隐式生存期类型(见 https://eel.is/c++draft/basic.types.general#9 与 https://eel.is/c++draft/class.prop#8):
- 它有用户声明的构造函数,因此它不是聚合类型;
- 它没有符合条件的平凡构造函数(它们都被删除)。
然而可以注意到 std::start_lifetime_as 要求其输出具有隐式生存期(甚至是一个聚合类型,如果我没看错,似乎有点超出必要)。因此,我会认为合理的结论是 std::bit_cast 只在输出时接受隐式生存期类型,而对输入类型是否需要约束则存在争议,因为输出值已经有不少约束。
我的分析有什么错吗?
注:这个问题并非完全理论性,因为不可构造的类也可以是必须仅用于继承的基实现类:
// Not constructible
// Not implicit lifetime
struct BaseImpl {
int val{1};
BaseImpl() = delete;
BaseImpl(BaseImpl const&) = delete;
BaseImpl(BaseImpl&&) = delete;
BaseImpl& operator=(BaseImpl const&) = default;
BaseImpl& operator=(BaseImpl&&) = default;
~BaseImpl() = default;
protected:
BaseImpl(int i) : val{i} {};
};
struct Concrete: private BaseImpl
{
Concrete(int i) : BaseImpl{i}{};
int Value() const {return BaseImpl::val;};
};
事后想法:特殊成员函数的访问限定符并不是平凡可拷贝或隐式生存期类型定义的一部分。因此这个类同时具备两者,这使得在是否存在更合适的要求时变得更难判定(如果有的话):
class Test {
int val;
Test() = default;
Test(Test const&) = default;
Test(Test&&) = default;
Test& operator=(Test const&) = default;
Test& operator=(Test&&) = default;
~Test() = default;
};
解决方案
要点在于C++为平凡可拷贝类型设定了一条特殊规则:它们的生命周期可以仅通过读取或拷贝它们的字节来开始。不需要调用构造函数。这就是 std::bit_cast 只检查平凡可拷贝性,而不检查隐式生存期状态的原因。
你的类删除了所有构造函数,但它仍然有一个平凡的赋值运算符,并且没有非平凡成员。这使得它成为平凡可拷贝的类型,而平凡可拷贝的类型被允许从原始存储中被构造。标准将读取这些字节的行为视为足以创建对象。
因此,尽管该类的设计似乎在暗示“你不应该能够创建这样的对象”,语言规则却并非如此。类型系统在类型是平凡可拷贝时,不存在阻止创建对象的办法。