在C++标准中,未定义行为段落中的[Note]是什么意思?
C++标准中的[Note]是解释当遇到没有定义行为的代码时,实现可能会做什么的。它并不是一种限制,而是给出一些常见行为的示例。
这个问题的出现是因为编译器几乎总是需要编译一些东西!考虑下面的代码片段:
void f() { 1 / 0; }
当翻译器遇到这段代码时,它的行为没有定义,但它不能随意做任何事情!实际上,如果它是一个编译器,它仍然需要编译这个编译单元。这是因为包含这个函数的程序的行为仍然是定义明确的!编译器无法知道这个函数是否被调用。实际上,这个问题是在函数为"main()"且控制流肯定会执行到除以零的地方时出现的,结果是编译器甚至不能拒绝这个程序。原因是:程序仍然是形式正确的,符合要求的编译器需要接受所有形式正确的程序(并拒绝所有形式不正确的程序并输出诊断错误消息,除非另有规定)。
这个问题很难使其形式不正确,因为很难精确地指定编译器在何时必须执行除以零的操作。
有趣的是,标准声称它"不会强制要求"的说法实际上非常接近错误。支持独立编译的编译系统的特点是它无法检测到没有定义行为的代码片段是否实际执行,因此编译器实际上被要求无论如何都要编译一些东西,因为它无法推断出这个程序是否有未定义行为。
不幸的是,如果编译器可以确定一个函数避免Undefined Behavior的唯一方法是调用可能在Undefined Behavior点之前退出程序的另一个函数,那么编译器将被允许无条件地使外部函数调用内部函数。
为什么这是不幸的呢?
这是不幸的,因为在许多情况下,程序员对于编译器在出现算术溢出等情况时可能做的任何事情都会感到满意,程序员必须添加代码以确定性地防止溢出发生。这样的代码会增加程序的复杂性,使其更难编写和阅读,并且在许多情况下会影响程序员本来可以接受的优化。如果一个程序可以指定"我需要编译器将溢出的后果限制为...任意选择以下一种",并且知道x=y+z
不会撤销因果律,这样的表达式将比x = (int)((unsigned)y+z);
更容易编写和阅读,而且根据程序员决定接受哪些选项,它还可以允许比后者更多的优化机会。例如,如果程序员表示溢出产生部分不确定值是可以接受的(它可能是一个行为像任意精度整数,其低32位是正确的,而高位可以是任何值),那么编译器可以安全地假设如果y
大于零,y+z
将大于z
(如果代码使用了unsigned
转换,这样的假设将被明确禁止)。我想一个编译器可以通过给出s < 32 ? v>>s : __INDETERMINATE ? 0 : v>>(s & 31)
来发现它可以使用单个移位指令实现这种行为,但我不认为这真的比v >> s
更好。
好吧,你想要的可以通过QoI,即实现质量来实现。虽然标准不要求任何东西,但是个别编译器供应商可以自由地指定一些东西,甚至可以选择行为。实际上,只有因为标准没有要求,所以才能够有这种自由。
问题是,很多情况下行为未定义是很难在静态上下文中检测到的,特别是像溢出、数组越界等情况。实际上,让我提到一个名为ATS的语言,它是由Howgwei Xi设计的应用相关类型,它提供了一种使用依赖类型来确保编译时没有数组越界的方法。程序员必须使用类型系统来提供证明。
从历史上看,优秀的C编译器通常会尽力提供比语言标准自身要求的更强的保证;语言对于未定义行为的任何形式都不需要做任何有用的事情,但为一些形式提供有用的行为,对于其他形式提供有用的陷阱,或者对于某些形式可能是可选择的选择,被认为比随机行为要好。从纯粹的基于要求的角度来说,说"实现必须指定对负数的左移可能执行的任何操作,但这样的规定可能包括未定义行为"......实际上与说"负数的左移会引发未定义行为"没有什么不同,因为标准对任何实现所要求的最多只是一段文档,而不是任何形式的可预测性。另一方面,从规范的角度来看,我认为标准的撰写者从来没有认为未定义行为是一件好事;一些系统可能具有设计不良的陷阱机制,这会阻止编译器提供更好的行为。引起Undefined Behavior的行为集合对于基于推理的优化来说是一个很差的基础,因为有很多情况下,程序员会对溢出产生部分不确定值或可识别的陷阱感到满意,但不能接受Undefined Behavior。强制程序员编写防止溢出的代码将使代码运行速度比允许溢出产生部分不确定值的情况下更慢。同样,如果有人可以采用略微放松的别名规则,那将对许多系统非常有用,该规则将表示别名实际上创建了其他变量,并且任何特定的写入或读取可能会任意访问其中任何一个变量。如果有一种构造可以使用,该构造可以指定在循环中读取的某些变量必须每次循环经过N次时至少重新加载一次。例如,如果N是5,循环展开10次,编译器在生成的循环中只需要刷新两次项目,而不是10次。这是一个重大的优势,但只有在实现定义了别名的效果时才有意义。
[Note]说明不是规范性的,它并不以任何方式限制UB。它只是一个澄清,即实现可以使用一些形式上导致UB的结构作为一种已记录的扩展,尽管依赖于此类细节的任何程序当然都不具备安全可移植性。
这个问题的出现的原因是:文档中的[Note]说明并不是规范性的,它只是对某些细节的澄清,这些细节可能导致未定义行为(Undefined Behavior,简称UB)。[Note]存在的目的是为了告诉开发人员,尽管某些构造在规范中会导致UB,但某些实现可能会以文档记录的形式将其作为扩展使用。这样的扩展在特定的实现中可能是有效的,但是在其他环境中可能无法正常运行。因此,如果一个程序依赖于这些细节,就不能保证在其他环境中能够安全地运行。
解决方法是:开发人员应该避免依赖于可能导致UB的细节。虽然某些实现可能支持某些扩展,但这些扩展并不是标准的一部分,不能保证在所有环境中都能正常运行。为了保证代码的可移植性和安全性,开发人员应该遵循C++标准中定义的规范,并避免使用可能导致UB的结构或行为。
以下是一些示例代码,展示了可能导致UB的一些情况:
int* ptr = nullptr; *ptr = 42; // Dereferencing a null pointer is undefined behavior
int arr[3]; int index = 3; int value = arr[index]; // Accessing an out-of-bounds array element is undefined behavior
开发人员应该注意避免这些情况的发生,以确保代码的正确性和可移植性。