Joe*_*oel 10 c++ memcpy language-lawyer c++17
这是代码:
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?)
Run Code Online (Sandbox Code Playgroud)
a
该标准是否保证在我们进行初始化的地方是一个确定的值c
?Cppreference说:
void* memcpy( void* dest, const void* src, std::size_t count );
将
count
字节从指向的对象复制到src
指向的对象dest
。这两个对象都重新解释为的数组unsigned char
。
但是我看不到cppreference中的任何地方,如果说这样不确定的值被“复制到”,它将变成确定的。
从标准看来,这类似于:
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?)
Run Code Online (Sandbox Code Playgroud)
看起来标准似乎为此留有余地,因为类型别名规则允许以此方式a
访问对象unsigned char
。但是我找不到能使它变得a
不确定的东西。
注意:我更新了这个答案,因为通过在一些评论中进一步探索这个问题,我发现了一些情况,在我最初没有考虑的情况下(特别是在 C++17 中),它将是实现定义的,甚至是未定义的。
\n\n我相信这在某些情况下是实现定义的行为,而在其他情况下是未定义的(正如另一个答案出于类似原因得出的结论)。从某种意义上说,如果它是未定义的行为或定义的实现,那么它的实现是定义的,所以我不确定它是否通常未定义在这样的分类中优先。
\n\n因为std::memcpy
完全适用于相关类型的对象表示(通过对unsigned char
6.10/8.8 [basic.lval] 指定的指针进行别名化)。如果所讨论的字节中的位unsigned long long
保证是特定的,那么您可以按照您的意愿操作它们,或者将它们写入任何其他类型的对象表示中。然后,目标类型将根据其值表示形式(无论是什么)使用这些位来形成其值,如 6.9/4 [basic.types] 中所定义:
\n\n\nT 类型对象的对象表示形式是 T 类型对象占用的 N\n 个 unsigned char 对象的序列,其中 N 等于 \n sizeof(T)。对象的值表示是保存类型 T 的值的一组位。对于普通可复制类型,值表示是对象表示中的一组位,\n 确定一个值,该值是一个实现定义的值集的离散元素。
\n
然后:
\n\n\n\n\n目的是 C++ 的内存模型与 ISO/IEC 9899 编程语言 C 的内存模型兼容。
\n
知道了这一点,现在最重要的是所讨论的整数类型的对象表示是什么。根据 6.9.1/7 [basic.fundemental]:
\n\n\n\n\n类型 bool、char、char16_t、char32_t、wchar_t 以及有符号和无符号整数类型统称为整型。整型的同义词是整型。整型类型的表示应使用纯二进制计数系统来定义值。[示例:本国际标准允许整数类型的两个\xe2\x80\x99s\n 补码、个\xe2\x80\x99 补码和带符号的幅度表示。\xe2\x80\x94 结束示例]
\n
然而,脚注确实澄清了“二进制计数系统”的定义:
\n\n\n\n\n使用二进制数字 0\n 和 1 的整数位置表示,其中连续位表示的值\n 是可加的,从 1 开始,并乘以连续的整数\n 2 次幂,位可能除外排名最高。\n(改编自美国国家信息处理系统词典\n。)
\n
我们还知道,无符号整数与有符号整数具有相同的值表示形式,只是根据 6.9.1/4 [basic.fundemental] 的模数以下:
\n\n\n\n\n无符号整数应遵守模 2^n 算术定律,其中 n\n 是该特定整数大小的值表示中的位数。
\n
虽然这并没有准确说明值表示可能是什么,但根据二进制计数系统的指定定义,连续位将按预期为 2 的相加幂(而不是允许位采用任何给定顺序),其中可能存在的符号位除外。此外,由于有符号和无符号值表示,这意味着无符号整数将存储为递增的二进制序列,直到 2^(n-1)(过去取决于如何处理有符号数,事物是实现定义的)。
\n\n然而,仍然存在一些其他考虑因素,例如字节顺序以及由于sizeof(T)
仅测量对象表示的大小而不是值表示的大小(如前所述),可能存在多少填充位。由于在 C++17 中没有标准方法(我认为)来检查字节顺序,因此这是导致其在结果中定义的实现的主要因素。至于填充位,虽然它们可能存在(但没有指定它们将在哪里,除了暗示它们不会中断形成整数值表示的连续位序列之外),写入它们可以证明存在潜在问题。由于 C++ 内存模型的目的是以“可比较”的方式基于 C99 标准的内存模型,因此 6.2.6.2 中的脚注(在 C++20 标准中作为注释引用,以提醒您它\'s 基于此)可以采取如下说法:
\n\n\n某些填充位组合可能会生成陷阱表示,例如,如果一个填充位是奇偶校验位。无论如何,对有效值的任何算术运算都不能生成陷阱表示,除非作为异常条件(例如溢出)的一部分,而无符号类型不会发生这种情况。填充位的所有其他组合都是值位指定的值的替代对象表示。
\n
据我所知,这意味着错误地直接写入填充位可能会生成陷阱表示。
\n\n这表明,在某些情况下,根据填充位是否存在以及字节顺序,结果可能会以实现定义的方式受到影响。如果填充位的某种组合也是陷阱表示,则这可能会变成未定义的行为。
\n\n虽然在 C++17 中不可能,但在 C++20 中,可以将与(C++17 中存在)或一些数学与, /以及这些类型std::endian
结合使用,以确保预期的字节序是正确的以及缺少填充位,允许根据先前建立的整数存储方式以定义的方式实际产生预期结果。当然,C++20 还进一步完善了这一点,并指定整数仅以二进制补码形式存储,从而消除了进一步的特定于实现的问题。std::has_unique_object_representations<T>
CHAR_BIT
UINT_MAX
ULLONG_MAX
sizeof