假设我去编译一些编写不佳的C ++源代码,这些源代码会调用未定义的行为,因此(正如他们所说)“任何事情都可能发生”。
从C ++语言规范在“合格”编译器中认为可接受的角度来看,这种情况下的“任何情况”是否包括编译器崩溃(或窃取我的密码,或者在编译时出现异常或错误),或者未定义行为的范围专门限于生成的可执行文件运行时会发生什么?
假设我去编译一些编写不佳的C ++源代码,这些源代码会调用未定义的行为,因此(正如他们所说)“任何事情都可能发生”。
从C ++语言规范在“合格”编译器中认为可接受的角度来看,这种情况下的“任何情况”是否包括编译器崩溃(或窃取我的密码,或者在编译时出现异常或错误),或者未定义行为的范围专门限于生成的可执行文件运行时会发生什么?
Answers:
未定义行为的规范定义如下:
本国际标准对此没有要求的行为
[注意:当本国际标准省略行为的任何明确定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的不确定行为包括从完全忽略情况以无法预测的结果到在翻译或程序执行过程中以环境特征的书面方式记录的行为(有无诊断消息)到终止翻译或执行(无问题)诊断消息)。许多错误的程序构造不会引起未定义的行为。他们需要被诊断。常量表达式的求值永远不会表现出明确指定为undefined的行为。—尾注]
尽管该说明本身不是规范性的,但它确实描述了已知表现出的一系列行为。因此,根据该说明,使编译器崩溃(翻译突然终止)是合法的。但是实际上,正如规范性文字所言,该标准对执行或翻译没有任何限制。如果实施方案窃取了您的密码,则不违反该标准中规定的任何合同。
我们通常担心的大多数UB,例如NULL-deref或被零除,都是运行时UB。编译一个函数,如果执行该函数将导致运行时UB不能导致编译器崩溃。 除非可能证明该功能(以及该功能的路径)一定会由程序执行。
(第二个想法:也许我没有考虑在编译时需要对template / constexpr进行评估。在此期间,UB可能会在翻译期间引起任意怪异,即使从未调用生成的函数也是如此。)
@StoryTeller的答案中ISO C ++引用的翻译过程中的行为类似于ISO C标准中使用的语言。C在编译时不包含模板或强制评估。constexpr
但有趣的事实是:ISO C在注释中表示,如果翻译终止,则必须带有诊断消息。或“在翻译过程中……以书面形式表现”。我不认为“完全忽略情况”可以理解为包括停止翻译。
旧答案,写于我了解翻译时UB之前。 但是,对于runtime-UB确实如此,因此可能仍然有用。
没有UB这样的事情在编译时发生。沿着特定的执行路径,编译器可以看到它,但是用C ++术语来说,直到执行通过函数到达该执行路径时,它才发生。
程序中甚至无法编译的缺陷不是UB,它们是语法错误。这样的程序在C ++术语中“格式不正确”(如果我的标准语言正确的话)。程序的格式可以正确,但包含UB。 未定义行为和格式错误的区别,无需诊断消息
除非我有误解,否则ISO C ++要求此程序正确编译和执行,因为执行永远不会达到零。(在实践中(Godbolt),好的编译器只会使可执行的可执行文件。gcc / clang会发出警告,x / 0
但不会发出警告,即使在优化时也不会发出警告。但是无论如何,我们试图告诉我们ISO C ++的低允许实现质量如何。 / clang除了确认我正确编写了程序外,几乎不是一个有用的测试。)
int cause_UB() {
int x=0;
return 1 / x; // UB if ever reached.
// Note I'm avoiding x/0 in case that counts as translation time UB.
// UB still obvious when optimizing across statements, though.
}
int main(){
if (0)
cause_UB();
}
一个用例可能涉及C预处理器,或constexpr
变量以及在这些变量上的分支,这会导致在某些路径中胡说八道,而对于那些常量选择,这些路径是永远不会达到的。
可以假定导致编译时可见的UB的执行路径绝不会采用,例如x86的编译器可能会发出ud2
(引起非法指令异常)作为的定义cause_UB()
。或在函数内,如果if()
线索的一侧导致可证明的UB,则可以删除分支。
但是编译器仍然必须以理智而正确的方式编译其他所有内容。如果C ++抽象机正在运行UB ,则所有未遇到(或无法证明遇到)UB的路径仍必须编译为执行asm的asm。
您可能会争辩说,无条件的编译时可见的UB inmain
是该规则的例外。 否则,编译时可证明,从开始执行main
实际上就可以达到保证的UB。
我仍然认为合法的编译器行为包括产生手榴弹,如果运行,它就会爆炸。或更合理的说,它的定义main
由一条非法指令组成。 我认为如果您从不运行该程序,那么还没有UB。 IMO不允许编译器本身爆炸。
分支内部包含可能或可证明的UB的函数
沿任何给定执行路径的UB都会向后退,以“污染”所有先前的代码。但是实际上,编译器只有在可以真正证明执行路径导致编译时可见的UB时,才能利用该规则。例如
int minefield(int x) {
if (x == 3) {
*(char*)nullptr = x/0;
}
return x * 5;
}
编译器必须使asm对x
3以外的所有对象都有效,直到x * 5
导致有符号溢出的UB达到INT_MIN和INT_MAX的程度。如果从不使用调用此函数x==3
,则该程序当然不包含UB,并且必须按编写的方式工作。
我们可能还用if(x == 3) __builtin_unreachable();
GNU C编写来告诉编译器x
肯定不是3。
实际上,普通程序中到处都有“雷区”代码。例如,任何整数除法都会向编译器保证其非零。任何指针deref都会向编译器保证它不是NULL。
“合法”在这里是什么意思?根据这些标准,任何与C标准或C ++标准不矛盾的东西都是合法的。如果您执行一条语句i = i++;
,结果恐龙占领了世界,那么这与标准并没有矛盾。但是它确实违背了物理定律,所以它不会发生:-)
如果未定义的行为使您的编译器崩溃,则不会违反C或C ++标准。但是,这确实意味着可以(可能应该)提高编译器的质量。
在以前的C标准版本中,有些语句是错误的或不依赖于未定义的行为:
char* p = 1 / 0;
允许将常数0分配给char *。不允许非零常量。由于1/0的值是未定义的行为,因此编译器是否应接受此语句是未定义的行为。(现在,1/0不再满足“整数常量表达式”的定义)。