不可恢复错误与编译器插入的检查
中文

不可恢复错误与编译器插入的检查

前面的章节其实已经见过两类失败了:

这一节讲的就是第二类。对今天的 scpp 来说,算术溢出、运行期下标越界之类的受检失 败都不会尝试恢复,而是会立刻终止整个程序。

对下面每个可以运行的例子,把文件保存成 fail-fast.scpp,然后这样编译运行:

scpp fail-fast.scpp -o fail-fast
./fail-fast

对于那些本来就应该中止的例子,程序本身会成功编译,但一旦真的碰到坏状态,就会通 过 std::abort() 结束运行。

运行期下标会在读取元素前先做检查

如果下标不是编译期常量,scpp 就没法在源码阶段直接拒绝程序。这时它会围绕真正的 访问点插入边界检查。

import std;

int read_at(std::span<const int> values, int index) {
    return values[index];
}

int main() {
    int numbers[4]{};
    numbers[0] = 10;
    numbers[1] = 20;
    numbers[2] = 30;
    numbers[3] = 40;
    std::println("{}", read_at(numbers, 2));
    return 0;
}

输出:

30

关键不只是 2 恰好合法,而是函数体里写的是普通的 values[index],即使精确下标要 等到运行期才知道,scpp 也仍然会把这次访问保持在受检状态里。

同一处受检下标在运行期值越界时会直接中止

如果同一处访问在运行时拿到了错误下标,插入进去的检查就会在任何非法读取发生之 前把程序停下来。

import std;

int read_at(std::span<const int> values, int index) {
    return values[index];
}

int main() {
    int numbers[4]{};
    numbers[0] = 10;
    numbers[1] = 20;
    numbers[2] = 30;
    numbers[3] = 40;
    return read_at(numbers, 9);
}

运行时行为:程序会直接中止,而不会去读取 span 末尾之外的数据。

这就是第 8.1 节那个“编译期越界报错”例子的运行期对应物。编译器如果能证明下标错 了,就直接拒绝源码;如果只有正在运行的程序才知道下标是多少,编译器就插入检查。

有符号整数溢出默认也是受检的

普通整数算术同样带检查。程序不会在溢出时悄悄回绕。

int main() {
    int limit = 2147483647;
    int next = limit + 1;
    return next;
}

运行时行为:程序会在计算 limit + 1 时直接中止。

所以对今天的 scpp 来说,这个边界上采用的是“快速失败”的受检算术,而不是 C++ 风 格的溢出惊喜或未定义行为。

除法会在真正计算前先检查除数

除法也会被检查。除数为零时,程序会直接中止,而不是把问题留给机器层面的未定义行 为。

int main() {
    int total = 10;
    int count = 0;
    return total / count;
}

运行时行为:程序会在真正做除法之前直接中止。

同一个插入检查也会覆盖另一个有符号除法陷阱,也就是 INT_MIN / -1,原因相同:结 果装不进一个 int

到目前为止,快速失败规则是

下一节仍然会留在“错误处理”这个主题里,但会把重心转向那些程序今天就能主动处理 的问题,也就是通过显式返回值来表达的可恢复错误。


← 上一章:用 std::span 借用视图 · 目录 · 下一章:今天可用的可恢复错误写法 →