编译器如何知道C++常量计算不会触发未定义的行为?

C++标准要求编译器在C++常量计算中检查未定义的行为。

在this talk中,Chandler Carruth指出,在检查UB时"您将耗尽检测错误的能力",而且在一般情况下,检测UB与halting problem相关,因此可以证明无法确定。

他指的不是conexpr中的UB,但conexpr计算从C++14开始就像常规程序一样通用,因此这仍然适用。

那么,当编译器无法确定程序是否是UB程序时,它们会做些什么?他们还会接受这个程序,祈祷自己继续编译吗?或者,他们更加保守,拒绝接受这项计划,即使它可能是正确的?(我个人的感觉是他们这样做)

对我来说,这很重要,因为我用Clang编译得很好的非平凡指针算法进行了常量计算,但用GCC编译失败了,我非常确定这不是UB。你可以说这是一个GCC的错误,但如果UB是无法决定的,那么所有的编译器在这方面都是错误的。

更根本的是,为什么标准要求无UB?有没有技术上的原因?或者更具哲理性的说法("如果编译器不能检查,程序员就会触发UB,就会产生不好的结果")?

我认为这与C++的其余部分不一致,C++的其余部分永远不会阻止您搬起石头砸自己的脚。我宁愿GCC接受我的常量代码并崩溃,或者如果是UB就发出垃圾;而不是当它不知道自己是不是UB时不编译。

= 编辑=

正如M.M和Nicol Bolas所指出的,该标准规定了限制(甚至在C++14中),因此我们永远不会遇到停顿问题类型的UB。然而,我仍然在想,检查UB是否可能太复杂,如果编译器启发式失败,那么他们会(可能不正确地)将其标记为非常量。

但我从评论中感觉到,这更多的是实现不成熟的问题。


解决方案

在这次演讲中,Chandler Carruth表示,在检查UB时"您将耗尽检测错误的能力",而且在一般情况下,检测UB与停止问题有关,因此可以证明不可能做出决定。

暂停的问题是,当您获取一个程序并尝试确定该程序在执行时是否一定会暂停。根据定义,停止问题仅将程序视为锁定对象。

持续评估是...评估。您正在执行程序,而不仅仅是查看源代码。

当程序的执行执行某些未定义的操作时,会发生未定义的行为。大多数UB案例不能确定是明确定义的,也不能仅仅通过检查源代码来确定。请考虑以下代码:

void foo(void *ptr)
{
  *reinterpret_cast<int*>(ptr) = 20;
}

那是不是UB?这要视情况而定;如果将指向int的指针传递到foo中,那么它将是定义良好的。此代码是否定义良好只能由执行方式确定。

常量计算需要执行代码;这就是我们通常将其称为编译时执行的原因。当您执行代码时,可以知道foo的特定执行是否传递了指向实际int的指针(忽略reinterpret_castconstexpr代码中被禁止的事实)。因此,在评估时,您可以知道是否发生了UB。

那么,当编译器无法确定程序是否是UB程序时,他们该怎么办?

这实际上不是一件可能发生的事情。假设规范是完整的并且没有漏洞,程序的执行是否表现出定义良好的行为仅仅是遵循规范的问题。

您与GCC和Clang之间的问题不是能否确定UB的问题。

更根本的是,为什么标准要求不含UB?是否有技术原因?

假设,我们可以从C++甚至C中删除所有未定义的行为。我们可以让一切都是先验定义的,并从语言中删除其计算结果不能从基本原则中确定的任何内容。

标准不会这样做,因为这会很糟糕。它会阻止我们做各种有用的、低级的事情。它将阻止有用的编译器优化。以此类推。

这些原因都不适用于编译时代码执行。尤其是"有用的、低级的东西"这一部分。对于编译后的代码,生成的代码在一台实际的机器上执行。因此,有一个后门来与真实的机器对话是有意义的。然而,在编译时,没有真正的机器可供对话;只有C++定义的抽象机器。那么允许UB有什么意义呢?

编译器不会生成并执行机器语言;常量求值基本上是在编译器内执行脚本语言。与大多数脚本语言一样,您希望它能够安全而正确地进行计算。您希望快速捕获错误(UB是错误),并在故障点提供干净的错误消息,而不是在过程中随意死亡。

相关文章