c++ - 从字节初始化一个由 trivially_copyable 但不是 default_con

我们正在从辅助存储中初始化(大型)trivially_copiable 对象数组,以及 this 等问题或 this让我们对我们实现的方法缺乏信心。

下面是一个最小的例子,试图说明代码中“令人担忧”的部分。 也请find it on Godbolt .

例子

让我们有一个 trivially_copyable 但不是 default_constructible 用户类型:

struct Foo
{
    Foo(double a, double b) :
        alpha{a}, 
        beta{b}
    {}

    double alpha;
    double beta;
};

信任 cppreference :

Objects of trivially-copyable types that are not potentially-overlapping subobjects are the only C++ objects that may be safely copied with std::memcpy or serialized to/from binary files with std::ofstream::write()/std::ifstream::read().

现在,我们要将一个二进制文件读入一个动态数组Foo。由于 Foo 不是默认可构造的,我们不能简单地:

std::unique_ptr<Foo[]> invalid{new Foo[dynamicSize]}; // Error, no default ctor

备选方案 (A)

使用未初始化的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";

是否存在 UB,因为实际类型 Foo 的对象从未在 storage 中显式创建?

备选方案 (B)

存储被显式类型化为 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";

此替代方案的灵感来自 this post .然而,它有更好的定义吗?似乎仍然没有显式创建 Foo 类型的对象。

在访问 Foo 数据成员时,值得注意的是摆脱了 reinterpret_cast(这个转换可能违反了 Type Aliasing rule )。

总体问题

  • 标准是否定义了这些替代方案?它们真的不同吗?

    • 如果没有,是否有正确的方法来实现它(无需首先将所有 Foo 实例初始化为将在之后立即丢弃的值)
  • C++ 标准版本之间的未定义行为是否存在差异? (具体请参阅 this comment 关于 C++20)

最佳答案

您最终要做的是通过 memcpy 从别处获取字节而不默认构造 T 创建某种类型的数组 T >首先在数组中。

Pre-C++20 在某些时候不引发 UB 就无法做到这一点。

问题最终归结为[intro.object]/1, which defines the ways objects get created :

An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created ([conv.rval], [class.temporary]).

如果你有一个 T* 类型的指针,但是在那个地址没有创建 T 对象,你不能假装这个指针指向一个实际 T。您必须使 T 产生,这需要执行上述操作之一。唯一可用于您的用途的是 new 表达式,它要求 T 是默认可构造的。

如果你想memcpy到这样的对象中,它们必须存在。所以你必须创造它们。对于此类对象的数组,这意味着它们需要是默认可构造的。

因此,如果可能的话,您需要一个(可能是默认的)默认构造函数。


在 C++20 中,某些操作可以隐式创建对象(引发“隐式对象创建”或 IOC)。 IOC 仅适用于隐式生命周期类型,for classes :

A class S is an implicit-lifetime class if it is an aggregate or has at least one trivial eligible constructor and a trivial, non-deleted destructor.

您的类符合条件,因为它有一个平凡的复制构造函数(即“eligible”)和一个平凡的析构函数。

如果创建字节类型数组(unsigned charstd::bytechar),this is said to "implicitly create objects" in that storage .此属性也适用于 mallocoperator new 返回的内存。这意味着如果您对指向该存储的指针执行某些类型的未定义行为,系统将自动创建对象(在创建数组的位置),使该行为得到明确定义。

因此,如果您分配这样的存储空间,将指向它的指针转换为 T*,然后开始使用它,就像它指向 T 一样,系统将自动在该存储中创建 T,只要它适当对齐即可。

因此,您的替代方案 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";

同样,storage[index] 创建了一个 T 数组,就好像它是在分配内存时创建的一样。

关于c++ - 从字节初始化一个由 trivially_copyable 但不是 default_constructible 对象组成的数组。 [intro.object] 中的混淆,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70082344/

相关文章:

ios - 如何获取 UTType 图像、音频和视频的所有扩展

flutter - 闪闪发光的动画

apache-kafka - 如何触发Kafka再平衡?

javascript - 显示: inline is used时隐藏元素符号

r - 在 dplyr 中跨列过滤

javascript - 左关联二叉树折叠

java - 在java中对字符串进行排序时如何忽略空格?

c++ - `shared_ptr::use_count() == 0` 和 `shared_ptr

ruby-on-rails - 如何一次为 Ruby 中的对象分配多个属性

function - 关于在函数和宏定义中使用结构文字