C/C++本身存在着大量的未定义行为,可能很多人都被这样的包含未定义行为的书或者考试坑过。。。比如:

printf("%d %d %d %d
", i++, ++i, ++i++, power(2, i));

个人认为,给出一段代码而没有一个确定的结果,是一个很可怕的事。

很多现代计算机语言,如Java等,都对很多未定义行为进行了规定。那么,在C/C++语言诞生的那个年代,为什么标准的编写者,不稍稍下一点气力,把标准里的那些undefined去掉呢?换句话说,是什么样的原因,比如历史原因,使得C/C++标准放任那么多的未定义行为而不去管呢?


主要原因:性能考虑,比如说一个简单例子,数组下标越界是ub,如果规定不是ub,而是一个固定的表现,比如抛异常,那么就像其他很多语言一样,每次操作时候检查,而且要知道,这个越界是针对数组而言的,比如你定义一个a[10],然后p=a[5],那么p[6]一样是越界,要想catch住所有类似情况,就比较麻烦了,你是选择穷遍所有platform来选择具体方式(比如利用访问非法内存产生中断信号,但这其实也不100%保证,万一越界后的内存还是合法的呢),还是选择将p实现为一个伪指针对象?实际上正常程序是不会越界的,越界时候明确异常只是方便调试,所以如果你写一个C编译器,可以将debug和release分开实现。很多ub的确可以从语言设计上确定其行为,只不过这样一来是以性能天花板的降低为代价的,这跟其他语言比就没啥优势了,C++只是选择将责任扔给程序员,这类ub还有strict-aliasing之类的

其他一些原因:历史原因,平台差异等,其他人也说的差不多了


有很多原因,例如说你问的这个主要是从性能优化的角度考虑的。例如说,你这例子,如果必须要有确定行为结果,那么就可能会产生额外的内存读写。而如果ub,那就允许编译器有更大的灵活性,例如说把i先加载到寄存器中进行计算,语句结束再统一回写。尤其是在古老的年代,计算机硬件水平还不发达,各种寄存器数量少而且五花八门的年代,硬性规定某种做法,会损害在某些平台上编译出来的代码的执行效率。


要不考虑下,如果严格定义所有指针值的读写行为会发生什么:

一般平台上的 C/C++ 形式上可以访问任意地址(把任意值的 uintptr_t 转型成指针后读写)。如果规定对非法指针值的读写不是 UB ,实现就必须准备好合法地址的记录(这可能导致内存占用多出数倍),并且在解引用指针时查询记录(只要指针来源分析断了,就导致不小的运行时开销),分配/释放时修改记录。

而且 C/C++ 有 strict aliasing 规则。这导致合法地址记录里还要能查到地址中的某个字节属于哪几种类型的对象(或是无对象的未初始化内存),是否首字节等。

这还只是指针带来 UB 的一角。可以继续想象下在 C 标准基础上直接消除 UB 会需要怎样的实现。

(另外 C/C++ 还规定某些无可观察副效应的无限循环是 UB ,其诊断等价于停机问题。不过可以直接改成不是 UB )

C++ 还有个 ill formed no diagnostic required ,有时其中的 "diagnostic" 只能通过逐个验证所有可能存在的类型才能完成,如 constexpr 配合模板时的情况。


完全去掉ub,我只能说你想多了,举个例子,你现在在写串口驱动,这个串口在硬件上直接接在了crossbar的某个地址段上,你想访问这个串口的控制寄存器,就得硬编码地址,这种行为是不可能良定义的。


因为程序本身是无法穷尽所在机器的一切状态的(停机问题),所以编译器也无法穷尽生成的程序所在目标机器上的一切状态。

C语言是工程师设计的,不是数学家。现实世界上的绝大多数问题是无法用算法解决的,工程师只想解决工程范围内的功能,而不是那么在乎全面的formal proof。


推薦閱讀:
相关文章