【问题标题】:std::bit_cast with std::array带有 std::array 的 std::bit_cast
【发布时间】:2020-02-07 17:53:39
【问题描述】:

在他最近的谈话“Type punning in modern C++” Timur Doumler said 中,std::bit_cast 不能用于将float 位转换为unsigned char[4],因为不能从函数返回 C 样式的数组。我们应该要么使用std::memcpy,要么等到C++23(或更高版本),当reinterpret_cast<unsigned char*>(&f)[i]之类的东西会变得很好定义时。

在 C++20 中,我们可以使用 std::array 和 std::bit_cast,

float f = /* some value */;
auto bits = std::bit_cast<std::array<unsigned char, sizeof(float)>>(f);

而不是 C 样式的数组来获取 float 的字节?

【问题讨论】:

    标签: c++ c++20 type-punning


    【解决方案1】:

    是的,这适用于所有主要编译器,据我查看标准,它是可移植的并且保证可以工作。

    首先,std::array&lt;unsigned char, sizeof(float)&gt; 保证是一个聚合 (https://eel.is/c++draft/array#overview-2)。由此得出,它在内部恰好包含chars 的sizeof(float) 数量(通常作为char[],虽然afaics 标准没有强制要求这种特定的实现 - 但它确实说元素必须是连续的)并且不能有任何额外的非静态成员。

    因此它很容易复制,并且它的大小也与float 的大小相匹配。

    这两个属性允许您在它们之间bit_cast。

    【讨论】:

    • 请注意,struct X { unsigned char elems[5]; }; 满足您引用的规则。它当然可以用最多 4 个元素进行列表初始化。它可以也用 5 个元素进行列表初始化。我不认为任何标准库的实现者讨厌人们实际这样做,但我认为它在技术上是一致的。
    • 谢谢! – 巴里,我认为这不太对。标准说:“最多可以用 N 个元素进行列表初始化”。我的解释是“最多”意味着“不超过”。这意味着你不能做elems[5]。那时我看不出你怎么会得到一个聚合,sizeof(array&lt;char, sizeof(T)&gt;) != sizeof(T)?
    • 我相信规则的目的(“可以列表初始化的聚合......”)是允许struct X { unsigned char c1, c2, c3, c4; }; 或struct X { unsigned char elems[4]; }; - 所以虽然字符需要是该聚合的元素,这允许它们成为直接聚合元素或单个子聚合的元素。
    • @Timur “最多”并不意味着“不超过”。同样,暗示P -&gt; Q 并不暗示!P 的情况
    • 即使聚合只包含正好 4 个元素的数组,也不能保证 array 本身不会有填充。它的实现可能没有填充(并且任何实现都应该被视为功能失调),但不能保证array 本身不会。
    【解决方案2】:

    接受的答案不正确,因为它没有考虑对齐和填充问题。

    每[array]/1-3:

    标题&lt;array&gt;定义了一个类模板,用于存储固定大小 对象序列。数组是一个连续的容器。一个实例 的array&lt;T, N&gt; 存储N 类型为T 的元素,因此size() == N 是一个不变量。

    数组是一个集合,最多可以用N 进行列表初始化 类型可转换为T的元素。

    数组满足容器和 可逆容器 ([container.requirements]),除了默认 构造的数组对象不为空并且该交换没有 恒定的复杂性。数组满足一些要求 序列容器。此处仅提供操作说明 在这些表之一中未描述的数组上,对于 有附加语义信息的操作。

    标准实际上并不要求std::array 有一个T[N] 类型的公共数据成员,因此理论上sizeof(To) != sizeof(From) 或is_­trivially_­copyable_­v&lt;To&gt; 是可能的。

    不过,如果这在实践中不起作用,我会感到惊讶。

    【讨论】:

      【解决方案3】:

      是的。

      根据描述std::bit_cast 行为的paper 及其proposed implementation,只要这两种类型具有相同的大小并且可以轻松复制,强制转换应该是成功的。

      std::bit_cast 的简化实现应该是这样的:

      template <class Dest, class Source>
      inline Dest bit_cast(Source const &source) {
          static_assert(sizeof(Dest) == sizeof(Source));
          static_assert(std::is_trivially_copyable<Dest>::value);
          static_assert(std::is_trivially_copyable<Source>::value);
      
          Dest dest;
          std::memcpy(&dest, &source, sizeof(dest));
          return dest;
      }
      

      由于浮点数(4 字节)和 unsigned char 和 size_of(float) 的数组尊重所有这些断言,因此将执行底层 std::memcpy。因此,结果数组中的每个元素都是浮点数的一个连续字节。

      为了证明这种行为,我在 Compiler Explorer 中编写了一个小示例,您可以在此处尝试:https://godbolt.org/z/4G21zS。浮点数 5.0 正确存储为字节数组 (Ox40a00000),对应于 Big Endian 中该浮点数的十六进制表示。

      【讨论】:

      • 您确定std::array 保证没有填充位等吗?
      • 不幸的是,某些代码有效这一事实并不意味着其中没有 UB。例如,我们可以写auto bits = reinterpret_cast&lt;std::array&lt;unsigned char, sizeof(float)&gt;&amp;&gt;(f) 并得到完全相同的输出。这能证明什么吗?
      • @L.F.根据规范:std::array 满足ContiguiosContainer (C++17 起) 的要求。
      • @ManuelGil: std::vector 也满足相同的标准,显然不能在这里使用。是否需要std::array 将元素保存在类内(在字段中),以防止它成为指向内部数组的简单指针? (就像向量中一样,它也有大小,字段中不需要有哪个数组)
      • @firda std::array 的聚合要求实际上需要它在里面存储元素,但我担心布局问题。
      猜你喜欢
      • 1970-01-01
      • 2020-02-03
      • 1970-01-01
      • 2022-10-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多