读取不确定的值会调用UB吗?

SO上的各种受尊敬的高代表用户坚持认为阅读具有不确定值的变量“总是UB”。 那么在C标准中究竟提到了哪一个呢?

很明显,不确定的值可能是未指定的值或陷阱表示:

3.19.2
不确定的价值
要么是未指定的值,要么是陷阱表示

3.19.3
未指明的价值
本国际标准对在任何情况下选择的值没有要求的相关类型的有效值
注意未指定的值不能是陷阱表示。

3.19.4
陷阱表示
一个对象表示,不需要表示对象类型的值

很明显,读取陷阱表示会调用未定义的行为,6.2.6.1:

某些对象表示不需要表示对象类型的值。 如果对象的存储值具有这样的表示,并且由不具有字符类型的左值表达式读取,则行为是未定义的。 如果这样的表示是由副作用产生的,该副作用通过不具有字符类型的左值表达式修改对象的全部或任何部分,则行为是未定义的.50)这种表示称为陷阱表示。

但是,不确定值不一定包含陷阱表示。 实际上,对于使用二进制补码的系统,陷阱表示非常罕见。

在C标准中,它实际上是说读取不确定的值会调用未定义的行为吗?

我正在阅读C11的非规范性附件J,并发现这确实列为UB的一个案例:

具有自动存储持续时间的对象的值在不确定时使用(6.2.4,6.7.9,6.8)。

但是,列出的部分无关紧要。 6.2.4仅规定有关生命时间和变量值变得不确定的规则。 类似地,6.7.9是关于初始化的,并说明变量的值如何变得不确定。 6.8似乎大多无关紧要。 这些部分都没有包含任何规范性文本,说访问不确定的值可能导致UB。 这是附件J中的缺陷吗?

然而,在关于左值的6.3.2.1中有一些相关的规范性文本:

如果左值指定了一个自动存储持续时间的对象,该对象可以使用寄存器存储类声明(从未使用其地址),并且该对象未初始化(未使用初始化程序声明,并且在使用之前未对其进行任何赋值) ),行为未定义。

但这是一种特殊情况,它仅适用于从未使用过地址的自动存储持续时间的变量。 我一直认为6.3.2.1的这一部分是UB关于不确定值(不是陷阱表示)的唯一情况。 但是人们一直坚持认为“它总是UB”。 那么究竟提到了哪里?

据我所知,标准中没有任何内容表明使用不确定值始终是未定义的行为。

拼写为调用未定义行为的案例是:

  • 如果值恰好是陷阱表示。
  • 如果不确定值是自动存储的对象。
  • 如果该值是指向其生命周期已结束的对象的指针。

例如,C标准指定unsigned char类型没有填充位,因此它的值都不能成为陷阱表示。

诸如memcpy之类的函数的可移植实现利用这一事实来执行任何值的副本,包括不确定的值。 当用作包含填充位的类型的值时,这些值可能是陷阱表示,但是当用作unsigned char值时,它们只是未指定。


我认为假设如果某些东西可以调用未定义的行为,那么当程序没有安全的检查方法时它调用未定义的行为是错误的。 请考虑以下示例:

 int read(int* array, int n, int i) { if (0 <= i) if (i < n) return array[i]; return 0; } 

在这种情况下, read函数没有安全的方法来检查array是否确实是(至少)长度为n 。 显然,如果编译器将这些可能的UB操作视为明确的UB ,那么编写任何指针代码几乎是不可能的。

更一般地说,如果编译器无法certificate某些东西是UB,则必须假设它不是UB,否则可能会破坏符合规范的程序。


唯一可能被视为确定性的情况是自动存储对象的情况。 我认为合理的原因是因为这些情况可以被静态拒绝,因为编译器需要的所有信息都可以通过本地流分析获得。

另一方面,将其声明为非自动存储对象的UB不会在编译或可移植性方面为编译器提供任何有用的信息(在一般情况下)。 因此,标准可能没有提到这些情况,因为它无论如何都不会改变实际实现中的任何内容。

为了实现优化机会和有用语义的最佳组合,没有陷阱表示的类型应将Indeterminate Values细分为三种:

  1. 第一次读取将产生任何可能由未指定的位模式产生的值; 后续将保证产生相同的价值。 这类似于“未指定的值”,除了标准通常不区分具有和不具有陷阱表示的类型,并且在标准要求“未指定的值”的情况下,它要求实现确保值不是陷阱表示; 在一般情况下,这将要求实现包括防止某些位模式的代码。

  2. 每次读取可以独立地产生可能由未指定的位模式产生的任何值。

  3. 读取的值以及对其执行的大多数计算的结果可能表现得非确定性,就像读取已经产生任何可能的值一样。

不幸的是,标准没有做出这样的区分,并且对它所要求的内容存在一些分歧。 我建议#2应该是默认值,但是代码应该可以指示代码需要强制编译器选择具体值的所有位置,并指示编译器可以在其他地方使用#3样式语义。 例如,如果将不同的16位值集合的代码存储为:

 struct COLLECTION { size_t count; uint16_t values[65536], locations[65536]; }; 

保持不变量,对于每个i

 uint32_t index = (uint32_t)(collection->locations[value]); if (index < collection->count && collections->values[index]==value) ... value was found 

每次从数组中读取一个项目时,上面的代码任意产生“index”的任意数字是可以接受的,但是第二行中“index”的使用都必须使用相同的值。

不幸的是,一些编译器编写者似乎认为编译器应该将所有不确定的值视为#3,而一些算法需要#1而一些算法需要#2,并且没有真正的方法来区分不同的需求。

3.19.2允许实现为陷阱表示,并且读取和写入都是未定义的行为。

您的平台可能会给您保证(例如,整数类型永远不会有陷阱表示),但标准不要求这样做 ,如果您依赖它,您的代码将失去一些可移植性。 这是一个有效的选择,但不应该无知。

更多系统具有浮点类型的陷阱表示而不是整数类型,但C程序可以在跟踪寄存器有效性的处理器上运行 – 请参阅(为什么)在C中使用未初始化的变量未定义行为? 。 这种程度的自由度是C在许多硬件架构中广泛采用的主要原因。