如果可能存在未定义行为,为什么编译器不警告您?

Why doesn't the compiler warn you if there is possible Undefined Behaviour?

我正在阅读著名的 Undefined Behavior can cause time travel post 并注意到这部分:

First of all, you might notice the off-by-one error in the loop control. The result is that the function reads one past the end of the table array before giving up. A classical compiler wouldn't particularly care. It would just generate the code to read the out-of-bounds array element (despite the fact that doing so is a violation of the language rules), and it would return true if the memory one past the end of the array happened to match.

A post-classical compiler, on the other hand, might perform the following analysis:

The first four times through the loop, the function might return true.

When i is 4, the code performs undefined behavior. Since undefined behavior lets me do anything I want, I can totally ignore that case and proceed on the assumption that i is never 4. (If the assumption is violated, then something unpredictable happens, but that's okay, because undefined behavior grants me permission to be unpredictable.)

根据这个 post,(较新的)编译器已经可以在编译时对未定义的行为采取行动,这意味着它在某些情况下完全能够发现未定义的行为。而不是通过消除 UB 代码或只是转换它来让恶魔飞出你的鼻子或产生龙,因为它是允许的,为什么编译器不只是发出这可能不是故意的警告?

编译器的工作是将代码从高级语言编译到低级语言。如果您收到描述性错误或警告消息,是时候感谢编译器为您做了额外的工作。要获得所需的警告,请使用一些 static code analysis 工具。

并且规范中没有明确定义的任何内容都是未定义的,并且不可能准备一份完整的未定义行为列表。可能无法对所有此类行为发出警告。

实际上,在许多情况下,编译器会警告未定义的行为,特别是带有适当的警告标志,如 gcc 上的 -W -Wall -Wextra -O2。 (使用 -O2 等优化标志,编译器会对代码进行回归分析,并可能生成更多警告)

编译器在诊断方面变得越来越好。但在主要代码分析不是他们必须做的工作。它只是送给你的礼物。

所以很常见:

要有多个编译器进行翻译并检查警告和错误(就像在带有 gcc 和 clang 的 jenkins 中)。

让静态代码分析也自动化(就像在 jenkins 中一样)

例如:

cppcheck main.cpp

结果:

[main.cpp:8]: (error) Array 'table[4]' accessed at index 4, which is out of bounds.

周围有很多工具,无论是否商业化。要同时查看 运行 时间效果,您可以给 valgrind 一个包含所有工具的机会。

是的,通过用测试工具替换部分标准库可以完成更多工作,例如用于内存消耗、堆栈跟踪等。请参阅 efenceduma 等。

但所有这些都不会自动找到您的所有错误。

必须进行单元测试(例如 gtest)并根据您的要求进行检查。这应该通过覆盖分析来完成。如果没有最后一个,您将不知道您真正测试了哪些代码以及 lines/branches 的哪些部分超出了您的测试控制范围。

编译器只是导致代码出错的一方面。

最后但同样重要的是:良好的代码同行评审非常有帮助!

在编译器假设程序永远不会接收会导致未定义行为的输入并使用该假设忽略会影响该输入处理方式的代码的情况下,尝试生成诊断的一个基本问题,当优化可以消除许多不同的代码片段时,最值得做的是优化,但在这些情况下,将产生的诊断数量将难以管理,以至于诊断无用。

我个人认为正确的方法是让编译器在许多情况下记录行为约束,在这些情况下,标准不会施加任何要求,但可以非常便宜地保证某些行为约束(*),但随后定义了指令以促进优化并表明程序员的意图。然后,静态分析工具可以识别消除程序员 可能 不关心的代码的地方可以提高效率,并允许程序员添加指令说 "Feel free to optimize on the blind assumption that condition xxx will be true" , 或 "Feel free to abnormally terminate the program if condition xxx is false, if not having to handle xxx would save code downstream", 或 "I need behavior to remain constrained to yyy degree if xxx is false." [如果静态分析工具再次为 运行,后一个指令将消除有关 xxx 的消息。

(*)例如如果 x==INT_MAX 保证 x+1>y 将产生 0 会很昂贵,但是保证表达式除了产生 1 或 0 之外永远不会做任何事情会很便宜;如果 1 或 0 满足要求,则该保证将允许程序员编写比其他方式更易于优化的代码。

如果 xxx 不为真意味着存在程序员应该知道的问题,建议编译器在不为真时终止程序的指令可以帮助程序员发现它。然而,超现代编译器设计可能会产生相反的效果,使编译器短路逻辑如下:

if (xxx) log_message("xxx was okay at checkpoint #57");

[如果 xxx 不为真会导致 UB 下游,编译器可以使日志消息调用无条件,从而使其看起来好像 x 在那个时候是真的,即使它不是]。将焦点转移到更多以程序员为中心的优化上(例如,如果一个程序包含许多 "Feel free to terminate the program if xxx is false")将允许编译器在程序员足够关心速度以提供此类指令的地方实现相同的优化,但没有编译器的行为模糊问题。