代码如下:
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 );
将
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?)
似乎标准为此留有余地,因为类型别名规则允许以这种方式将对象a
作为unsigned char
进行访问。但是我找不到能使a
不再不确定的东西。
答案 0 :(得分:4)
这不是未定义的行为
是UB,因为您要复制到错误的类型。 [basic.types]2 and 3允许字节复制,但只能在相同类型的对象之间进行。您已从long long
复制到int
。这与不确定的值无关。即使您仅复制sizeof(int)
个字节,但您并非从实际的int
复制一个事实,这意味着您没有得到这些规则的保护。
如果要复制到相同类型的值,则[basic.types] 3表示等同于简单地分配它们。也就是说,a
“应随后具有与” b
相同的值。
答案 1 :(得分:1)
TL; DR:由实现定义,是否将存在未定义的行为。证明样式,代码行编号:
unsigned int a;
假定变量a
具有自动存储持续时间。其寿命开始(6.6.3 / 1)。由于它不是类,因此其生存期始于默认初始化,在该初始化中,不执行任何其他初始化(9.3 / 7.3)。
unsigned long long b = 1ull;
假定变量b
具有自动存储持续时间。其寿命开始(6.6.3 / 1)。由于它不是类,因此其生存期始于复制初始化(9.3 / 15)。
std::memcpy(&a, &b, sizeof(unsigned int));
根据16.2 / 2,std::memcpy
应该具有与C标准库的memcpy
相同的语义和前提条件。在C标准7.21.2.1中,假设sizeof(unsigned int) == 4
,将4个字符从&b
指向的对象复制到&a
指向的对象。 (这两点是其他答案所缺少的。)
至此,unsigned int
,unsigned long long
的大小,其表示形式(例如字节序)和字符的大小都已实现定义(据我所知,请参见6.7.1 / 4及其说明说适用ISO C 5.2.4.2.1)。我将假设实现是小端的,unsigned int
是32位,unsigned long long
是64位,字符是8位。
现在我已经说过实现是什么,我知道a
的{{1}}值为1u。到目前为止,没有什么是不确定的行为。
unsigned int
现在,我们访问unsigned int c = a;
。然后,6.7 / 4说
对于普通可复制类型,值表示形式是对象表示形式中确定值的一组位,该值是实现定义的一组值中的一个离散元素。
我现在知道a
的值是由a
中实现定义的值位确定的,我知道它们保留1u的值表示形式。 a
的值为1u。
然后像(2)一样,变量a
被复制初始化为1u。
我们利用实现定义的值来查找发生的情况。实现定义的1ull值可能不是c
的实现定义的一组值之一。在那种情况下,访问unsigned int
将是未定义的行为,因为标准不会说明当您使用无效的值表示形式访问变量时会发生什么。
AFAIK,我们可以利用以下事实:大多数实现都定义了a
,其中任何可能的位模式都是有效的值表示形式。因此,不会有未定义的行为。
答案 2 :(得分:0)
注意:我更新了此答案,因为在某些评论中通过进一步探讨该问题揭示了在我最初未考虑过的情况下(特别是在C ++ 17中)将实现定义或什至未定义的情况
我认为这在某些情况下是实现定义的行为,而在另一些情况下则是未定义的(因为出于类似原因而得出另一个答案)。从某种意义上说,它是由实现定义的,如果它是未定义的行为或实现的定义,那么我不确定在这种分类中,一般来说,未定义是优先考虑的。
由于std::memcpy
完全适用于所讨论类型的对象表示(通过别名给unsigned char
的指针进行别名,如6.10 / 8.8 [basic.lval]所指定)。如果保证unsigned long long
的字节中的位是特定的,则可以根据需要进行操作,也可以将它们写入任何其他类型的对象表示中。然后,目标类型将根据6.9 / 4 [basic.types]中定义的值表示形式(无论可能是什么)使用这些位来形成其值:
类型T的对象的对象表示形式是N的序列 T类型的对象占用的无符号char对象,其中N等于 sizeof(T)。对象的值表示形式是一组位 包含类型T的值。对于平凡可复制的类型,该值 表示是对象表示中的一组位, 确定一个值,该值是 实施定义的一组值。
那:
目的是C ++的内存模型与C ++的内存模型兼容 ISO / IEC 9899编程语言C。
知道了这一点,现在最重要的是所讨论的整数类型的对象表示形式是什么。根据6.9.1 / 7 [basic.fundemental]:
键入bool,char,char16_t,char32_t,wchar_t以及带符号和 无符号整数类型统称为整数类型。一种 整数类型的同义词是整数类型。的表示 整数类型应使用纯二进制数定义值 系统。 [示例:此国际标准允许两个 补码,补码和有符号幅值表示 整数类型。 —结束示例]
但是脚注确实阐明了“二进制计算系统”的定义:
使用二进制数字0的整数的位置表示形式 和1,其中连续位代表的值是 加法器,从1开始,然后乘以连续积分 2的幂次幂,也许最高位置的位除外。 (改编自《美国国家信息词典》 处理系统。)
我们还知道,无符号整数与有符号整数具有相同的值表示形式,只是处于6.9.1 / 4 [basic.fundemental]的模数之下:
无符号整数应服从2 ^ n模算术定律,其中n 是该特定值的值表示形式中的位数 整数的大小。
虽然这不能确切说明值的表示形式,但根据二进制计算系统的指定定义,连续的位应按预期为2的相加幂(而不是允许位以任何给定的顺序排列) ),可能存在的符号位除外。另外,由于有符号和无符号值表示形式,这意味着无符号整数将作为递增的二进制序列存储,直到2 ^(n-1)(然后根据所处理的有符号数的方式来定义实现)。
但是,还有其他一些考虑,例如字节序和sizeof(T)
仅测量对象表示的大小而不是值表示(如前所述)可能导致多少填充位。由于在C ++ 17中没有(我认为)检查字节序的标准方法,因此这是将其定义为实现结果的主要因素。至于填充位,尽管它们可能存在(但没有指明它们的位置,但我暗示它们不会中断形成整数值表示形式的连续位序列),但可以对其进行写操作证明可能有问题。由于C ++内存模型的意图是以“可比较的”方式基于C99标准的内存模型,因此来自6.2.6.2的脚注(在C ++ 20标准中作为注解被引用,提醒它基于),内容如下:
填充位的某些组合可能会生成陷阱表示, 例如,如果一个填充位是奇偶校验位。无论如何 对有效值进行算术运算会生成陷阱 除作为特殊条件的一部分外,例如 溢出,并且这种情况不会发生在无符号类型上。所有其他 填充位的组合是 由值位指定的值。
这意味着错误地直接写入填充位可能会根据我的判断产生陷阱表示。
这表明在某些情况下,取决于是否存在填充位和字节序,结果可能会以实现定义的方式受到影响。如果填充位的某种组合也是陷阱表示,则可能会变成不确定的行为。
虽然在C ++ 17中无法实现,但在C ++ 20中,可以将std::endian
与std::has_unique_object_representations<T>
(在C ++ 17中存在)结合使用,或与{{1 }},CHAR_BIT
/ UINT_MAX
和ULLONG_MAX
这些类型,以确保期望的字节序正确且不存在填充位,从而在定义的字段中实际产生期望的结果给出了以前建立的关于如何存储整数的方式。当然,C ++ 20还对此进行了进一步完善,并指定整数应单独存储在二进制补码中,从而消除了进一步的特定于实现的问题。