不可恢復錯誤與編譯器插入的檢查
前面的章節其實已經見過兩類失敗了:
- 有些程式會在編譯期直接被拒絕,因為編譯器已經能證明它是錯的;以及
- 有些操作雖然會通過編譯,但 scpp 會先插入一個執行期檢查,一旦執行真的走到不可能 或不安全的狀態,程式就會立刻中止。
這一節講的就是第二類。對今天的 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。
到目前為止,快速失敗規則是
- 如果 scpp 只靠原始碼本身就能證明某個操作有問題,它會在編譯期直接拒絕程式;
- 如果正確性取決於執行期值,scpp 可能會插入一個檢查,並在值非法時直接中止程式;
- 受檢索引、受檢算術溢出、受檢除法,都是這種快速失敗模型的例子;
- 在今天的語言模型裡,這些都屬於不可恢復錯誤,而不是可以捕獲後繼續執行的例外。
下一節仍然會留在「錯誤處理」這個主題裡,但會把重心轉向那些程式今天就能主動處理 的問題,也就是透過明確回傳值來表達的可恢復錯誤。