std::memcpy 是否使其目的地确定?

Does std::memcpy make its destination determinate?

代码如下:

unsigned int a;            // a is indeterminate
unsigned long long b = 1;  // b is initialized to 1
std::memcpy(&a, &b, sizeof(unsigned int));
unsigned int c = a;        // Is this not undefined behavior? (Implementation-defined behavior?)

标准是否保证 a 是我们访问它以初始化 c 的确定值? Cppreference says:

void* memcpy( void* dest, const void* src, std::size_t count );

Copies count bytes from the object pointed to by src to the object pointed to by dest. Both objects are reinterpreted as arrays of unsigned char.

但是我在 cppreference 中没有看到任何地方说如果一个不确定的值是 "copied to" 像这样,它就会变成确定的。

从标准来看,好像是这样的:

unsigned int a;            // a is indeterminate
unsigned long long b = 1;  // b is initialized to 1
auto* a_ptr = reinterpret_cast<unsigned char*>(&a);
auto* b_ptr = reinterpret_cast<unsigned char*>(&b);
a_ptr[0] = b_ptr[0];
a_ptr[1] = b_ptr[1];
a_ptr[2] = b_ptr[2];
a_ptr[3] = b_ptr[3];
unsigned int c = a;        // Is this undefined behavior? (Implementation defined behavior?)

似乎标准允许这样做,因为类型别名规则允许对象 a 以这种方式作为 unsigned char 访问。但我找不到说明这使得 a 不再不确定的内容。

Is this not undefined behavior

它是 UB,因为您正在复制到错误的类型。 [basic.types]2 and 3 允许字节复制,但仅限于相同类型的对象之间。您从 long long 复制到 int。这与不确定的价值无关。即使您只复制 sizeof(int) 个字节,但您不是从实际 int 复制的事实意味着您得不到这些规则的保护。

如果您正在复制到相同类型的值,那么 [basic.types]3 表示这相当于简单地分配它们。也就是说,a " 随后应与 " b.

保持相同的值

注意:我更新了这个答案,因为通过在一些评论中进一步探索这个问题已经揭示了在我最初没有考虑的情况下它会被实现定义甚至未定义的情况(特别是在 C++17 中嗯)。

我相信这在某些情况下是实现定义的行为,而在其他情况下是未定义的(因为另一个答案出于类似原因得出结论)。从某种意义上说,如果它是未定义的行为或已定义的实现,则它是已定义的实现,因此我不确定它是否通常未定义在此类分类中优先。

因为 std::memcpy 完全适用于所讨论类型的对象表示(通过别名给 unsigned char 的指针,如 6.10/8.8 [basic.lval] 所指定)。如果保证 unsigned long long 的相关字节中的位是特定的,那么您可以随意操作它们或将它们写入任何其他类型的对象表示中。然后,目标类型将根据其值表示(无论可能是什么)使用这些位来形成其值,如 6.9/4 [basic.types]:

中所定义

The object representation of an object of type T is the sequence of N unsigned char objects taken up by the object of type T, where N equals sizeof(T). The value representation of an object is the set of bits that hold the value of type T. For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values.

还有那个:

The intent is that the memory model of C++ is compatible with that of ISO/IEC 9899 Programming Language C.

知道了这一点,现在重要的是所讨论的整数类型的对象表示是什么。根据 6.9.1/7 [basic.fundemental]:

Types bool, char, char16_t, char32_t, wchar_t, and the signed and unsigned integer types are collectively called integral types. A synonym for integral type is integer type. The representations of integral types shall define values by use of a pure binary numeration system. [Example: This International Standard permits two’s complement, ones’ complement and signed magnitude representations for integral types. — end example ]

脚注确实阐明了 "binary numeration system" 的定义,但是:

A positional representation for integers that uses the binary digits 0 and 1, in which the values represented by successive bits are additive, begin with 1, and are multiplied by successive integral power of 2, except perhaps for the bit with the highest position. (Adapted from the American National Dictionary for Information Processing Systems.)

我们还知道无符号整数与有符号整数具有相同的值表示,只是根据 6.9.1/4 [basic.fundemental]:

Unsigned integers shall obey the laws of arithmetic modulo 2^n where n is the number of bits in the value representation of that particular size of integer.

虽然这并没有确切说明值表示可能是什么,但根据二进制记数系统的指定定义,连续的位将如预期的那样是 2 的相加幂(而不是允许位以任何形式出现)给定顺序),可能存在的符号位除外。此外,由于有符号和无符号值表示,这意味着无符号整数将存储为递增二进制序列,直到 2^(n-1)(过去取决于如何处理有符号数,实现定义)。

然而,还有一些其他的考虑因素,例如字节顺序和可能存在的填充位数量,因为 sizeof(T) 仅测量对象表示的大小而不是值表示(如前所述)。由于在 C++17 中没有标准的方法(我认为)来检查字节序,所以这是将其留给实现定义在结果中的主要因素。至于填充位,虽然它们可能存在(但没有指定它们的位置,除了暗示它们不会中断形成整数值表示的连续位序列之外,我可以告诉它们),写入它们可以证明有潜在问题。由于 C++ 内存模型的意图是以 "comparable" 方式基于 C99 标准的内存模型,因此来自 6.2.6.2 的脚注(在 C++20 标准中作为注释被引用以提醒它基于在那)可以采取如下说法:

Some combinations of padding bits might generate trap representations, for example, if one padding bit is a parity bit. Regardless, no arithmetic operation on valid values can generate a trap representation other than as part of an exceptional condition such as an overflow, and this cannot occur with unsigned types. All other combinations of padding bits are alternative object representations of the value specified by the value bits.

这意味着直接写入填充位不正确可能会产生陷阱表示,据我所知。

这表明在某些情况下,根据是否存在填充位和字节顺序,结果可能会受到 implementation-defined 方式的影响。如果填充位的某种组合也是陷阱表示,这可能会成为未定义的行为。

虽然在 C++17 中不可能,但在 C++20 中,可以将 std::endianstd::has_unique_object_representations<T>(在 C++17 中出现)结合使用,或者将一些数学运算与 CHAR_BITUINT_MAX/ULLONG_MAXsizeof 这些类型以确保预期的字节顺序正确以及没有填充位,从而实际产生预期的结果给定先前确定的整数存储方式的定义方式。当然,C++20 还进一步改进了这一点,并指定整数将单独存储在二进制补码中,从而消除了进一步的 implementation-specific 问题。

TL;DR: implementation-defined 是否会有未定义的行为。 Proof-style,代码行编号:


  1. unsigned int a;

假定变量a具有自动存储期限。它的生命周期开始 (6.6.3/1)。由于它不是 class,它的生命周期从默认初始化开始,其中不执行其他初始化 (9.3/7.3)。

  1. unsigned long long b = 1ull;

假定变量b具有自动存储期限。它的生命周期开始 (6.6.3/1)。由于它不是 class,它的生命周期从 copy-initialization (9.3/15) 开始。

  1. std::memcpy(&a, &b, sizeof(unsigned int));

根据 16.2/2,std::memcpy 应具有与 C 标准库的 memcpy 相同的语义和前提条件。在C标准7.21.2.1中,假设sizeof(unsigned int) == 4,将&b指向的对象中的4个字符复制到&a指向的对象中。 (这两点是其他答案所缺少的。)

至此,unsigned intunsigned long long的大小、它们的表示(例如字节顺序)和字符的大小都是实现定义的(据我了解,请参见6.7.1 /4 及其说明适用 ISO C 5.2.4.2.1)。我假设实现是little-endian,unsigned int是32位,unsigned long long是64位,一个字符是8位。

既然我已经说了实现是什么,我知道 a 对于 1u 的 unsigned int 有一个 value-representation。到目前为止,没有任何未定义的行为。

  1. unsigned int c = a;

现在我们访问a。然后,6.7/4 表示

For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values.

我现在知道 a 的值由 a 中的 implementation-defined 值位决定,我知道它保持 value-representation 1u。那么a的值就是1u。

那么和(2)一样,变量c就是copy-initialized到1u.


我们使用 implementation-defined 值来找出发生了什么。 1ull 的 implementation-defined 值可能 而不是 unsigned int 的 implementation-defined 值集之一。在这种情况下,访问 a 将是未定义的行为,因为标准没有说明当您使用无效的 value-representation 访问变量时会发生什么。

AFAIK,我们可以利用这样一个事实,即大多数实现都定义了一个 unsigned int,其中任何可能的位模式都是有效的 value-representation。因此,不会有未定义的行为。