为什么不定义一些未定义的行为?

mer*_*ian 4 c++ undefined-behavior

C++ 不定义某些行为(例如更好的错误检查)的原因是什么?为什么不抛出一些错误并停止呢?

一些伪代码例如:

if (p == NULL && op == deref){
    return "Invalid operation"
 }
Run Code Online (Sandbox Code Playgroud)

对于整数溢出:

if(size > capacity){
    return "Overflow"
}
Run Code Online (Sandbox Code Playgroud)

我知道这些都是非常简单的例子。但我很确定大多数 UB 都能被编译器捕获。那么为什么不实施它们呢?因为它确实很耗时并且不进行错误检查更快?一些 UB 可以通过单个 if 语句捕获。那么也许速度并不是唯一的问题?

use*_*751 9

因为每次使用指针时编译器都必须添加这些指令。C++ 程序使用大量指针。所以会有很多这样的指令供计算机运行。C++ 的哲学是您不应该为不需要的功能付费。如果需要空指针检查,可以编写空指针检查。制作一个自定义指针类来检查它是否为空非常容易。

在大多数操作系统上,取消引用空指针肯定会导致程序崩溃。整数溢出一定会回绕。C++ 不仅可以在这些操作系统上运行 - 它也可以在其他不支持的操作系统上运行。因此,该行为不是在 C++ 中定义的,而是在操作系统中定义的。


尽管如此,一些编译器编写者意识到,通过再次取消定义,他们可以使程序更快。因此,可以进行数量惊人的优化。有时会有一个命令行选项告诉编译器不要取消定义行为,例如-fwrapv.

常见的优化是简单地假设程序永远不会到达具有 UB 的部分。就在昨天,我看到一个问题,有人写了一个显然不是无限循环的:

int array[N];
// ...
for(int i = 0; i < N+1; i++) {
    fprintf(file, "%d ", array[i]);
}
Run Code Online (Sandbox Code Playgroud)

但它是无限的。为什么?问问题的人已经打开了优化。编译器可以看到最后一次迭代中有 UB,因为array[N]超出了范围。所以它假设程序在最后一次迭代之前神奇地停止了,所以它不需要检查最后一次迭代并且可以删除该部分i < N+1

大多数时候,由于宏或内联函数,这是有意义的。有人写了这样的代码:

int getFoo(struct X *px) {return (px == NULL ? -1 : px->foo);}

int blah(struct X *px) {
    bar(px->f1);
    printf("%s", px->name);
    frobnicate(&px->theFrob);
    count += getFoo(px);
}
Run Code Online (Sandbox Code Playgroud)

并且编译器可以做出非常合理的假设,即px不为空,因此它删除了px == NULL检查并getFoo(px)px->foo. 这通常就是为什么编译器编写者选择保持它未定义,即使它可以很容易地定义。


Hol*_*Cat 5

编译器已经有开关来启用这些检查(这些检查称为消毒剂,例如-fsanitize=address-fsanitize=undefined在 GCC 和 Clang 中)。

标准总是要求这些检查是没有意义的,因为它们会损害性能,因此您可能不希望在发布版本中使用它们。

  • @merovingian 他们的意思是不可能在编译时检查。消毒剂在运行时起作用。 (3认同)