重新解释数据会是未定义的行为吗?

Would reinterpreting data be undefined behavior?

最近有人提出这个:

uint8_t a = 0b10000000;
int8_t b = *(int8_t*) &a;

是未定义的行为,因为 a 的值超出了我可以在 int8_t 中表示的值。有人可以解释为什么这是未定义的行为吗?

我的主要问题是内存在那里,并且作为 int8_t 的内存是有效的,唯一的区别是 int8_t 会将那个字节解释为 -128,而uint8_t 会将其解释为 128。我对此感到更加困惑,因为快速平方根反比使用:

float y =  /* Some val*/;
int32_t i  = * ( int32_t * ) &y; 

这将给出 i 的值,本质上与 y 无关(除了 IEEE 标准),所以我不明白为什么重新解释一段内存可能是未定义的行为。

感谢所有评论。我掉进了一个 strict aliasing and found that the fast inverse square root is undefined behavior, despite my beliefs, but my initial code does not seem to be. Not because uint8_t is special, but as the standard 的兔子洞 有一个规则 signed/unsigned 交换它:

If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined: [...] (11.2) a type that is the signed or unsigned type corresponding to the dynamic type of the object

所以理论上没有问题,因为uint8_tint8_t

的无符号类型

问题不是数据的重新解释,而是指针的重新解释。这是有问题的,原因如下:

  • 标准不要求所有指针大小相同,所以sizeof(float*)不一定是sizeof(int*),所以转换可能只是丢失数据。
  • 如果您从 float* 中获取 uint32_t* 并从中读取,您将读取从未创建的 uint32_t
  • 如您所说,编译器假设两个不同类型的指针(unsigned char* 除外)从不别名,并使用此信息执行优化。

但是,有时在不相关类型的位表示之间进行转换 合法的要求。传统上,这是使用 memcpy 完成的,但是 C++20 增加了 std::bit_cast,即使在 constexpr 中也可以进行这种重新解释,所以下面是合法的,直接表达了我们的意图:

constexpr float pi = 3.14f;
constexpr uint32_t pi_bits = std::bit_cast<uint32_t>(pi);

C 和 C++ 标准的作者并没有尝试定义完成每项似是而非的任务所需的所有行为,而是允许实现支持或不支持各种有用的行为,在他们闲暇时,假定编译器编写者将能够比委员会更好地了解和支持客户的需求。

如果一个平台的目标是所有指针都具有相同大小和相同表示的平台(当前处理器和控制器设计的几乎所有实现都是如此),则确保用于访问特定对象的任何指针type 满足平台对该类型的对齐要求(如果指针是最大基元大小的倍数,则为真),并且使用指定支持直接类型双关模式的编译器配置(例如 -fno-strict-aliasing on clang或 gcc),然后类型双关代码将在该编译器配置上按预期工作。这样的代码将无法移植到所有其他实现或配置,但可移植性只是判断代码质量的一个因素。如果代码将 运行 在将要使用它的所有 C 实现上高效且正确地使用,将其替换为较慢的代码 and/or 更难阅读纯粹为了使其“可移植”的目的不会是一种改进.

顺便说一句,我测试过的每个编译器配置要么使用支持超出标准强制要求的有用类型双关结构的抽象模型,要么未能支持标准强制支持的所有内存回收结构.编译器不可能在标准定义行为的所有情况下都按照指定的方式行事,而在标准没有强加任何要求的许多情况下,编译器也不可能以与写入和读取对象表示一致的方式行事;据推测,标准的作者希望编译器通过在比标准要求更多的情况下表现得更有用来解决这个困难,但是当启用优化时,clang 和 gcc 优先考虑“优化”而不是正确性。