我们知道未定义的行为是什么,我们(或多或少)知道大多数行为的原因(性能,跨平台兼容性)。假设一个给定的平台,比如Windows 32位,我们可以将未定义的行为视为众所周知的并在整个平台上保持一致吗?我理解没有一般性答案然后我会限制为两个常见的UB 我经常在生产代码中看到(使用多年)。
1)Reference。提供此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)Reference。给定一组无符号short*
转换为(例如)float*
并访问它(reference,数组成员之间没有填充)。
代码依赖众所周知的 UB(如那些)按案例 (假设编译器可能会更改,并且肯定编译器版本将会肯定会改变)或者即使他们是UB的跨平台代码,他们也依赖于平台特定的细节(如果我们不改变平台,它就不会改变)?同样的推理也适用于未指定的行为(当编译器文档没有说明任何内容时)?
编辑根据this post从C99开始,双关语只是未指定,而不是未定义。
答案 0 :(得分:3)
未定义的行为主要是一个非常简单的事情,所讨论的代码的行为没有定义,所以C标准没有提供任何可能发生的线索。不要搜索超过它的内容。
如果C标准没有定义某些内容,那么您的平台可能会作为扩展程序进行定义。因此,如果您遇到这种情况,可以在该平台上使用它。但是请确保他们记录了该扩展,并确保他们不会在下一版本的编译器中对其进行更改。
由于多种原因,您的示例存在缺陷。正如评论中所讨论的那样,union
用于类型惩罚,特别是对任何字符类型的访问都是允许的。你的第二个例子真的很糟糕,因为除了你似乎暗示的,这在我知道的任何平台上都不是一个可接受的演员。 short
和float
通常具有不同的对齐属性,使用这样的东西几乎肯定会使程序崩溃。然后,第三,你在Windows上争论C,因为他们不遵循C标准而闻名。
答案 1 :(得分:2)
首先,任何编译器实现都可以自由地定义它在任何情况下所喜欢的行为,从标准的角度来看,它会产生未定义的行为。
其次,为特定编译器实现编写的代码可以自由地使用该实现记录的任何行为;但是,这样做的代码可能无法在其他实现中使用。
C的长期缺点之一是,虽然有很多情况下某些实现上的Undefined Behavior可能会被其他人有效地处理,但只有极少数这种情况提供了代码可以指定编译器的任何方法。哪些不会以某种方式处理它们应该拒绝编译。此外,在许多情况下,标准委员会允许全面使用UB,即使在大多数实施中,“自然”后果将受到更多限制。例如,考虑(假设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
语句都不会成为真,因此可能会认为r
始终为零。 请注意,其中一个推断会影响在未定义行为之前的语句的行为。
我真正希望看到的一件事是“实施约束”行为的概念,这将是未定义行为和实现定义行为之间的交叉:编译器需要记录所有可能的行为某些结构的后果在旧规则下将是未定义的行为,但是 - 与实现定义的行为不同 - 不需要实现来指定将发生的特定事件;实现将被允许指定某个构造可能具有任意无约束的后果(完整的UB)但不鼓励这样做。在类似整数溢出的情况下,合理的折衷方案是说溢出的表达式的结果可能是一个“魔术”值,如果显式类型转换,将产生任意(和“普通”)所指示类型的值,但是否则可能具有任意变化的值,这些值可能代表也可能不代表。可以允许编译器假设操作的结果不是溢出的结果,但是不会推断操作数。使用模糊的类比,如果明确地对NaN
进行类型转换可能会产生任意非NaN结果,那么行为将类似于浮点数。
int
大小,实现约束行为等的组合],将会很有帮助。可以为任何平台编写一个编译器,根据请求,它可以使促销规则表现得好像int
正好是32位。例如,给定代码如:
uint64_t l1,l2; uint32_t w1,w2; uint16_t h1,h2;
...
l1+=(h1+h2);
l2+=(w2-w1);
如果使用16位在h1
和h2
上执行数学运算,则16位编译器可能最快;如果将其添加到l2
,则64位编译器可能最快从w1
减去w2
的64位结果,但是如果代码是为32位系统编写的,那么能够为其他两个系统编译代码就会生成代码在32位系统上比生成执行某些不同计算的代码更有帮助,无论后者代码的速度有多快。
不幸的是,目前还没有任何标准方法可以让代码要求这样的语义[在很多情况下可能会限制64位代码的效率];最好的方法可能是在某处明确记录代码的环境要求,并希望使用代码的人看到它们。