我们通常担心的大多数 UB,例如 NULL-deref 或除以零,都是 runtime UB。编译一个会导致运行时 UB 的函数如果执行一定不会导致编译器崩溃。除非它可以证明该函数(以及通过该函数的路径)肯定会 由程序执行。
(第二个想法:也许我没有考虑在编译时需要评估模板 / constexpr。可能在此期间 UB 允许在翻译过程中导致任意怪异,即使从未调用结果函数。)
ISO C++ 引用in @StoryTeller's answer 的翻译过程中的行为部分类似于 ISO C 标准中使用的语言。 C 在编译时不包含模板或constexpr 强制评估。
但有趣的事实:ISO C 在注释中说,如果终止翻译,则必须带有诊断消息。或“在翻译过程中表现......以有记录的方式”。我不认为“完全无视情况”可以理解为包括停止翻译。
旧答案,在我了解翻译时 UB 之前写的。不过,对于运行时 UB 来说确实如此,因此可能仍然有用。
在编译时没有发生这样的事情。它可以沿着特定的执行路径对编译器可见,但在 C++ 术语中,直到执行通过函数到达该执行路径时它才发生。 p>
程序中甚至无法编译的缺陷不是 UB,而是语法错误。这样的程序在 C++ 术语中是“格式不正确的”(如果我的标准正确的话)。程序可以是格式良好但包含 UB。 Difference between Undefined Behavior and Ill-formed, no diagnostic message required
除非我有什么误解,否则 ISO C++要求这个程序能够正确编译和执行,因为执行永远不会被零除。 (在实践中 (Godbolt),好的编译器只会生成可运行的可执行文件。gcc/clang 会警告 x / 0,但不会对此发出警告,即使在优化时也是如此。但无论如何,我们正试图说明如何低 ISO C++ 允许实现质量。因此,检查 gcc/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,则可以删除分支。
但是编译器仍然必须以理智和正确的方式编译所有else。所有没有遇到(或无法证明遇到)UB 的路径仍必须编译为 asm,该 asm 就像 C++ 抽象机正在运行它一样执行。
您可以争辩说main 中的无条件编译时可见 UB 是此规则的一个例外。 或者编译时可证明从 main 开始的执行实际上确实达到了保证 UB。
我仍然认为,合法的编译器行为包括产生爆炸 if 运行的手榴弹。或者更合理的是,main 的定义由一条非法指令组成。 我认为如果你从不运行程序,那么还没有任何 UB。编译器本身不允许爆炸,IMO。
在分支内包含可能或可证明的 UB 的函数
UB 沿着任何给定的执行路径及时向后“污染”所有以前的代码。但在实践中,编译器只有在能够证明执行路径导致编译时可见的 UB 时才能利用该规则。例如
int minefield(int x) {
if (x == 3) {
*(char*)nullptr = x/0;
}
return x * 5;
}
编译器必须生成适用于除 3 之外的所有 x 的 asm,直到 x * 5 在 INT_MIN 和 INT_MAX 处导致有符号溢出 UB。如果这个函数从不使用x==3 调用,那么程序当然不包含UB,并且必须按照编写的方式工作。
我们不妨在 GNU C 中编写 if(x == 3) __builtin_unreachable(); 来告诉编译器 x 绝对不是 3。
实际上,普通程序中到处都有“雷区”代码。例如任何除以整数都会向编译器保证它是非零的。任何指针 deref 都向编译器保证它不是 NULL。