Ad *_*d N 2 c++ strict-aliasing undefined-behavior language-lawyer
我们正在trivially_copiable从辅助存储中初始化(大型)对象数组,诸如此类的问题使我们对所实现的方法缺乏信心。
下面是一个最小的示例,试图说明代码中“令人担忧”的部分。也请在 Godbolt 上找到它。
让我们有一个trivially_copyable但不是default_constructible用户类型:
struct Foo
{
Foo(double a, double b) :
alpha{a},
beta{b}
{}
double alpha;
double beta;
};
Run Code Online (Sandbox Code Playgroud)
信任cppreference:
不存在潜在重叠子对象的普通可复制类型的对象是唯一可以使用 std::memcpy 安全复制或使用 std::ofstream::write()/std::ifstream 与二进制文件序列化的 C++ 对象::读()。
现在,我们想要将二进制文件读入动态数组中Foo。由于Foo不可默认构造,我们不能简单地:
std::unique_ptr<Foo[]> invalid{new Foo[dynamicSize]}; // Error, no default ctor
Run Code Online (Sandbox Code Playgroud)
使用未初始化的unsigned char数组作为存储。
std::unique_ptr<unsigned char[]> storage{
new unsigned char[dynamicSize * sizeof(Foo)] };
input.read(reinterpret_cast<char *>(storage.get()), dynamicSize * sizeof(Foo));
std::cout << reinterpret_cast<Foo *>(storage.get())[index].alpha << "\n";
Run Code Online (Sandbox Code Playgroud)
是否存在 UB,因为实际类型的对象Foo从未在 中显式创建storage?
存储显式地键入为Foo.
std::unique_ptr<Foo[]> storage{
static_cast<Foo *>(::operator new[](dynamicSize * sizeof(Foo))) };
input.read(reinterpret_cast<char *>(storage.get()), dynamicSize * sizeof(Foo));
std::cout << storage[index].alpha << "\n";
Run Code Online (Sandbox Code Playgroud)
这个替代方案是受到这篇文章的启发。然而,它有更好的定义吗?似乎仍然没有显式创建 type 的对象Foo。
值得注意的是,它消除了reinterpret_cast访问Foo数据成员时的(此强制转换可能违反了类型别名规则)。
标准是否定义了这些替代方案?它们实际上不同吗?
C++ 标准版本之间的未定义行为有什么区别吗?(特别是,请参阅有关 C++20 的评论)
您最终要做的是通过从其他地方获取字节T来创建某种类型的数组memcpy,而不是先默认构造T数组中的 s 。
C++20 之前的版本无法在不引发 UB 的情况下做到这一点。
问题最终归结为[intro.object]/1,它定义了创建对象的方式:
当隐式更改联合的活动成员或创建临时对象([conv.rval],[class.temporary])时,通过定义、new 表达式创建对象。
如果您有一个类型为 的指针T*,但T在该地址中尚未创建任何对象,则不能假装该指针指向实际的T。您必须使其T成为现实,这需要执行上述操作之一。唯一可以满足您的目的的是 new 表达式,它要求 是T默认可构造的。
如果你想memcpy进入这样的对象,它们必须首先存在。所以你必须创建它们。对于此类对象的数组,这意味着它们需要默认可构造。
因此,如果可能的话,您需要一个(可能是默认的)默认构造函数。
在 C++20 中,某些操作可以隐式创建对象(引发“隐式对象创建”或 IOC)。IOC 仅适用于隐式生命周期类型,对于类:
如果类 S 是一个聚合或者至少具有一个普通的合格构造函数和一个普通的、未删除的析构函数,则它是隐式生命周期类。
您的类符合资格,因为它有一个简单的复制构造函数(“合格”)和一个简单的析构函数。
如果您创建一个字节类型数组(unsigned char、std::byte或char),则称为在该存储中“隐式创建对象”。malloc此属性也适用于和返回的内存operator new。这意味着,如果您对指向该存储的指针执行某些类型的未定义行为,系统将自动创建对象(在创建数组的位置),从而使该行为得到明确定义。
因此,如果您分配这样的存储,将指向它的指针转换为 a T*,然后开始使用它,就像它指向 a 一样T,系统将自动T在该存储中创建 s ,只要它适当对齐即可。
因此,你的替代方案 A 工作得很好:
当您应用[index]强制转换指针时,C++ 将在该存储中追溯创建一个数组Foo。也就是说,因为您像使用Foo存在数组一样使用内存,所以 C++20 将在那里创建存在数组Foo,就像您在语句中创建它一样new unsigned char。
然而,替代方案 B 不会按原样工作。您没有使用它new[] Foo来创建该数组,因此您无法使用delete[] Foo它来删除它。您仍然可以unique_ptr使用,但您必须创建一个显式调用operator delete指针的删除器:
struct mem_delete
{
template<typename T>
void operator(T *ptr)
{
::operator delete[](ptr);
}
};
std::unique_ptr<Foo[], mem_delete> storage{
static_cast<Foo *>(::operator new[](dynamicSize * sizeof(Foo))) };
input.read(reinterpret_cast<char *>(storage.get()), dynamicSize * sizeof(Foo));
std::cout << storage[index].alpha << "\n";
Run Code Online (Sandbox Code Playgroud)
再次storage[index]创建一个数组,T就好像它是在分配内存时创建的一样。