何时可以将未定义行为视为众所周知和被接受的?(这是一个关于IT技术的提问标题)

3
我们知道什么是未定义行为,而且我们(或多或少)知道大多数未定义行为的原因(性能、跨平台兼容性)。假设给定平台,比如 Windows 32 位,我们可以认为未定义行为在该平台上是“众所周知的”并且在整个平台上保持一致吗?我明白“没有一个通用的答案”,那么我将“限制于两个常见的UB”,我经常在生产代码中看到它们(使用多年)。
1) 参考。给出这个union
union {
    int value;
    unsigned char bytes[sizeof(int)];
} test;

像这样初始化:

test.value = 0x12345678;

然后使用以下方式访问:
for (int i=0; i < sizeof(test.bytes); ++i)
    printf("%d\n", test.bytes[i]);

2) 参考。给定一个无符号 short* 数组,将其转换为(例如)float* 并访问它(参考, 数组成员之间没有填充)。

代码是否依赖于已知的 UBs(如那些)按情况工作(假设编译器可能会改变,并且肯定会改变编译器版本),即使它们是跨平台代码的 UB,它们也依赖于特定于平台的细节(那么如果我们不改变平台,它就不会改变)?同样的推理是否也适用于未指定的行为(当编译器文档没有任何说明时)?

编辑 根据这篇文章,从 C99 开始类型游戏只是未指定的,而不是未定义的


3
C90规定这是实现定义。C99有点模糊,但意图是允许类型转换(参见DR283)。 - mafso
1
就我所知,32位平台会强制 sizeof(void*) == 4,但不一定会强制 sizeof(int) == 4 - barak manos
2
@barakmanos 你是对的! - Adriano Repetti
3
在C11标准中,这是脚注95(在§6.5.2.3节):“如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则将该值的相应部分的对象表示重新解释为新类型的对象表示,如6.2.6所述(有时称为“类型转换”过程)。” - Some programmer dude
1
@BennoZeeman 未定义行为 - Some programmer dude
显示剩余28条评论
2个回答

3

未定义行为主要意味着一件非常简单的事情,特定代码的行为未定义,因此C标准没有提供任何可以发生什么的线索。不要在其中寻找更多。

如果C标准没有定义某些内容,则您的平台可能会作为扩展进行定义。因此,如果您处于这种情况下,可以在该平台上使用它。但是请确保他们记录了该扩展,并且他们不会在编译器的下一个版本中更改它。

您的示例有多个缺陷。如评论讨论的那样,联合体是用于类型转换的,特别是对内存的任何字符类型的访问始终是允许的。您的第二个示例非常糟糕,因为除了您似乎在暗示之外,我不知道有哪个平台可以接受这种强制转换。 short和float通常具有不同的对齐属性,使用这种方法几乎肯定会使程序崩溃。然后,第三,您正在支持C on Windows,而众所周知Windows不遵循C标准。


1
  1. 好的,使用联合体进行类型转换是未指定的(从C99开始)。顺便说一句,联合体不是用来共享内存的吗?类型转换在20多年后才出现-UB...
  2. 一个短填充的数组BETWEEN元素,而不仅仅是在末尾吗?
  3. 就这样,但是这个平台我会限制这个问题(主要是因为它运行在较小的硬件架构上)。
- Adriano Repetti
  1. 你为什么认为这是未指定的?标准明确允许访问union的不同字段。类型转换自C语言诞生之初就存在,特别是在C语言设计的Unix系统中。
  2. 数组永远不会填充。
  3. 我不理解你的句子。
- Jens Gustedt
  1. 连接帖子中提到:在 C99 之前,它存在但肯定是未定义的(总是有效?也许......这就是我在这里问的!)
  2. 对,那么对齐不是问题(在 X86 上,我将问题限制为单个架构)。
  3. 在 Windows 上有时会出现奇怪的问题,但这就是我看到过这样的代码的地方(尽管我认为它不仅限于 Win32 架构,因为关于 punning 的帖子表明了这一点),所以我将我的问题限制在该环境中。
- Adriano Repetti
@AdrianoRepetti,对于(2)对齐要求模型,其中奇偶校验地址可能位于其上。short通常有2个字节,因此地址要求通常是最低有效位为0float通常有4个字节,因此两个较低位通常必须为0 - Jens Gustedt
我的意思是:常见的例子是写入char(例如从流中读取),然后读取int(但我所说的适用于所有其他兼容类型,假设值不会陷阱)。在X86对齐不是问题(可能会减慢访问速度,但是允许使用),因此我理解的是,如果它是UB但_stable_可用(假设平台不会更改)。主要是因为我看到了_so much_像这样的代码... - Adriano Repetti

2
首先,任何编译器实现都可以在任何情况下自由地定义任何行为,这些行为从标准的角度来看会产生未定义的行为。
其次,针对特定编译器实现编写的代码可以自由地使用该实现记录的任何行为;然而,这样做的代码可能无法在其他实现上使用。
C语言长期以来的缺点之一是,虽然有许多情况下,某些构造可能会在某些实现上导致未定义行为,但只有少数情况提供了任何方式,使得代码可以指定一个不会以某种方式处理它们的编译器应该拒绝编译。此外,在许多情况下,标准委员会允许出现完全未定义的行为,即使在大多数实现中,“自然”的后果会受到更多限制。例如,请考虑以下情况(假定 int 为32位)。
int weird(uint16_t x, int64_t y, int64_t z)
{
  int r=0;
  if (y > 0) return 1;
  if (z < 0x80000000L) return 2;
  if (x > 50000) r |= 31;
  if (x*x > z) r |= 8;
  if (x*x < y) r |= 16;
  return r;
}

如果上述代码在一个忽略整数溢出的机器上运行,传递50001,0,0x80000000L应该导致代码返回31; 传递50000,0,0x80000000L可能会导致它返回0、8、16或24,具体取决于代码如何处理比较操作。然而,C标准允许代码在任何这些情况下做任何事情;因此,一些编译器可能会确定除了前两个if语句外的所有if语句都无法在未引发未定义行为的情况���成立,并因此假定r始终为零。请注意,其中一种推论将影响Undefined Behavior之前的语句的行为
我真正想看到的是“实现约束”行为的概念,它将是Undefined Behavior和Implementation-Defined Behavior之间的一种交叉:编译器将需要记录某些构造的所有可能的后果,而不像Implementation-Defined behavior那样,实现不需要指定一个特定的事情会发生;实现将被允许指定某个构造可能具有任意无限制的后果(完全UB),但是应该避免这样做。对于像整数溢出这样的情况,一个合理的妥协是说,溢出的表达式的结果可能是一个“神奇”的值,如果显式类型转换,则会产生所指定类型的任意(和“普通”)值,但否则可能看起来具有任意变化的值,这些值可能或可能不可表示。编译器将允许假设操作的结果不是溢出的结果,但将避免对操作数进行推断。用一个模糊的类比来说,行为类似于如果显式类型转换NaN可以产生任何非NaN结果时的浮点。
在我看来,C语言将极大地受益于将上述“实现约束”行为概念与一些标准预定义宏相结合,这些宏允许代码测试实现在各种情况下是否做出了特定的承诺。此外,如果有一种标准手段可以通过代码部分请求特定的“方言”[组合的int大小、实现约束行为等]将会很有帮助。例如,可以编写一个编译器,使其在请求时将提升规则的行为表现得好像int正好是32位。例如,给定以下代码:
uint64_t l1,l2; uint32_t w1,w2; uint16_t h1,h2;
...
l1+=(h1+h2);
l2+=(w2-w1);

一个16位编译器如果使用16位对h1和h2进行数学运算可能是最快的,如果一个64位编译器将w1从w2中减去并将结果加到l2中,则可能是最快的。但是,如果代码是为32位系统编写的,那么让其他两个系统的编译器生成的代码与在32位系统上的行为相同将比让它们生成执行某些不同计算的代码更有帮助,无论后者的速度有多快。
不幸的是,目前没有任何标准手段可以使代码请求这样的语义[这一事实可能会限制64位代码在许多情况下的效率];最好的方法可能就是明确地在某个地方记录代码的环境要求,并希望使用代码的人能看到它们。

Impl-constrained 听起来有点像未指定。 - Damian Yerrick
@DamianYerrick:实现受限相当于在那些表明会遵守行为保证的实现上是未指定的,在那些表明不做出这种承诺的实现上是未定义的。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接