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

时间:2018-07-12 08:22:33

标签: c++ language-lawyer c++17 undefined-behavior strict-aliasing

很长时间以来,我一直在使用std::memcpy来规避严格别名

例如,检查this之类的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

但是,这一次,我检查了标准,但没有发现任何可验证此标准的东西。我发现的只是this

  

对于平凡可复制的类型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
     

-示例]

this

  

对于任何平凡可复制的类型T,如果指向T的两个指针指向不同的T对象obj1和obj2,则如果组成obj1的基础字节([intro.memory])都不是可能重叠的子对象,则obj1和obj2都不是可能重叠的子对象复制到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
     

-示例]

因此,允许std::memcpyfloat进行char[]的往返操作,也允许在相同的琐碎类型之间进行std::memcpy的操作。

我的第一个示例(以及链接的答案)是否定义正确?或者检查float的正确方法是将std::memcpy放入unsigned char[]缓冲区中,并使用shiftor s来构建{{1 }}?


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

3 个答案:

答案 0 :(得分:21)

该标准可能无法正确地说明允许这样做,但是据我所知,所有实现都将其视为已定义的行为。

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

来自char[N]的代表uint32_t值的字节可以复制到uint32_t对象中。我相信这部分也没有争议。

我认为同样毫无争议的是fwrite可能已经在程序的一次运行中写入了字节,而fread可能已经在另一次运行甚至是另一程序中完全读取了字节。

由于最后一部分,我相信字节来自哪里都没有关系,只要它们形成某些uint32_t对象的有效表示即可。您可以 遍历所有float值,并在每个值上使用memcmp,直到获得所需的表示形式为止,您知道该表示形式将与{{1} }您将其解释为。您甚至可以在另一个程序中做到这一点,这是编译器从未见过的程序。那本来是有效的。

如果从实现的角度来看,您的代码与明确有效的代码没有区别,则必须将您的代码视为有效。

答案 1 :(得分:18)

  

我的第一个示例(以及链接的答案)是否定义正确?

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

可移植性问题:

  • C ++不保证IEEE 754
  • 不能保证float的字节字节序与整数字节序匹配。
  • (具有陷阱表示形式的系统)。

您可以使用DB::rollback()来验证关于表示的假设是否正确。

尽管std::numeric_limits::is_iec559似乎没有陷阱(请参见注释),所以您不必担心。通过使用uint32_t,您已经排除了对深奥系统的可移植性-不需要使用符合标准的系统来定义该别名。

答案 2 :(得分:13)

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

  

从src指向的对象中复制count个字节到该对象   dest指出。 两个对象都重新解释为的数组   unsigned char

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


  

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

但是,由于标准float的值是实现定义的,因此不能保证此标准(但是对于IEEE 754,它可以工作)。