std :: memcpy是否在不同的可复制类型之间是未定义的行为?

gez*_*eza 52 c++ strict-aliasing undefined-behavior language-lawyer c++17

我一直在std::memcpy用来规避严格的混叠很长一段时间.

例如,检查a float,像这样:

float f = ...;
uint32_t i;
static_assert(sizeof(f)==sizeof(i));
std::memcpy(&i, &f, sizeof(i));
// use i to extract f's sign, exponent & significand
Run Code Online (Sandbox Code Playgroud)

但是,这次,我检查了标准,我还没有找到任何可以验证这一点的东西.我所发现的是这个:

对于平凡可复制类型T的任何对象(可能重叠的子对象除外),无论对象是否保持类型T的有效值,组成对象的基础字节([intro.memory])都可以复制到char,unsigned char或std :: byte([cstddef.syn])数组.40如果将该数组的内容复制回对象,则该对象应随后保持其原始值.[例如:

#define N sizeof(T)
char buf[N];
T obj;                          // obj initialized to its original value
std::memcpy(buf, &obj, N);      // between these two calls to std?::?memcpy, obj might be modified
std::memcpy(&obj, buf, N);      // at this point, each subobject of obj of scalar type holds its original value
Run Code Online (Sandbox Code Playgroud)

- 结束例子]

:

对于任何简单的可复制类型T,如果指向T的两个指针指向不同的T对象obj1和obj2,其中obj1和obj2都不是潜在重叠的子对象,如果构成obj1的基础字节([intro.memory])被复制到OBJ2,41 OBJ2随后应保持相同的值OBJ1.[例如:

T* t1p;
T* t2p;
// provided that t2p points to an initialized object ...
std::memcpy(t1p, t2p, sizeof(T));
// at this point, every subobject of trivially copyable type in *t1p contains
// the same value as the corresponding subobject in *t2p
Run Code Online (Sandbox Code Playgroud)

- 结束例子]

因此,允许std::memcpy使用floatto/from char[],并且允许std::memcpy在相同的普通类型之间进行.

我的第一个例子(和链接的答案)是否定义明确?或者检查a的正确方法float是将std::memcpy其放入unsigned char[]缓冲区,并使用shifts和ors从中构建一个uint32_t


注意:查看std::memcpy保证可能无法回答这个问题.据我所知,我可以std::memcpy用简单的字节复制循环替换,问题也是一样的.

小智 22

标准可能无法正确说明这是允许的,但几乎可以肯定,并且据我所知,所有实现都会将此视为已定义的行为.

为了便于复制到实际char[N]对象中,组成f对象的字节可以像访问它一样被访问char[N].我认为,这一部分没有争议.

char[N]表示uint32_t值的字节可以复制到uint32_t对象中.我认为,这一部分也没有争议.

我相信,同样无可争议的是,例如,fwrite可能已经在程序的一次运行中写入了字节,并且fread可能已经在另一次运行中读取它们,或者甚至完全在另一个程序中读取它们.

由于最后一部分,我认为字节来自何处并不重要,只要它们形成某个uint32_t对象的有效表示即可.您可以循环使用所有float值,使用memcmp每个值,直到获得所需的表示,然后您知道它将与uint32_t您将其解释为的值相同.你可能甚至已经做了在另一个程序,编译器从来没有见过的程序.这本来是有效的.

如果从实现的角度来看,您的代码与明确有效的代码无法区分,那么您的代码必须被视为有效.

  • @ PeterA.Schneider对.有"如果该数组的内容被复制回对象,则该对象应随后保持其原始值." 它允许将*复制回*一个简单的可复制对象,但是标准中从未明确给出了复制到(而不是返回)一个简单可复制对象的一般权限,它只能被推断出来.这是我答案的要点. (4认同)
  • 但是,OP观察到的结果很有趣:尽管6.9.2中的标准明确允许从一个普通的可复制对象中复制字节,但它缺少(或似乎缺少-我只是看了n4659中所有出现的`memcpy`),明确的规则,允许将字节复制到这样的对象中。它可能被认为是自我理解的。毕竟,6.9.2中的示例本身会将字节复制回去。 (2认同)

eer*_*ika 18

我的第一个例子(和链接的答案)是否定义明确?

行为未定义(除非目标类型具有不由源类型共享的陷阱表示),但整数的结果值是实现定义的.标准不保证如何表示浮点数,因此无法以可移植的方式从整数中提取尾数等 - 也就是说,使用系统将自己限制在IEEE 754并不会限制你这么多天.

便携性问题:

  • C++不保证IEEE 754
  • float的字节字节顺序不保证与整数字节序匹配.
  • (带陷阱表示的系统).

您可以std::numeric_limits::is_iec559用来验证您对表示的假设是否正确.

虽然看起来uint32_t没有陷阱(见评论)所以你不必担心.通过使用uint32_t,您已经排除了对深奥系统的可移植性 - 标准的符合系统不需要定义该别名.

  • @StoryTeller是正确的,所以确切的宽度类型别名有保证(除了它们是别名的类型,如果它们别名标准`int`等).这有点奇怪,但能够忽略陷阱总是很好,所以我会接受它:) (2认同)
  • "这种行为并非未定义".为什么?标准中是否有任何定义? (2认同)
  • 是的,他们举例说明了他们之前在规范性文本中所写的内容.我可以使用简单的字节复制循环复制字节,结果将是相同的.所以重点不是"memcpy",而是原则.我可以通过一种方法逐字节地将`float`复制到`uint32_t`吗?它是由标准定义的吗? (2认同)
  • 许多实现在寄存器和内存中以不同方式表示某些数据类型.例如,在32位RISC平台上常见的是,放置在寄存器中的`uint16_t`将具有16个数据位和16个填充位,这些位在写入值时归零.如果在未初始化时读取这样的对象,则它可能产生一个值,该值有时表现得像在0-65535范围之外,但有时表现得像在该范围内.这种行为可以追溯到安腾之前几十年. (2认同)

Som*_*ken 14

您的示例定义明确,不会破坏严格的别名.std::memcpy明确指出:

count将src指向的对象中的字节复制到dest指向的对象.这两个对象都被重新解释为数组 unsigned char.

该标准允许通过a (signed/unsigned) char*或任何类型别名std::byte,因此您的示例不显示UB.如果得到的整数具有任何值,则是另一个问题.


use i to extract f's sign, exponent & significand

然而,标准并不保证这一点,因为a的值float是实现定义的(在IEEE 754的情况下,它将起作用).