P.6: 編譯時無法做的檢查應當在運行時做
原因
留下難以檢測的錯誤在程序中會導致崩潰和糟糕的結果。
Note
理想情況下我們在編譯時或運行時捕獲所有的錯誤(非程序員的邏輯錯誤),然而並不可能在編譯時捕獲所有的錯誤,通常也不值得在運行捕獲所有剩餘的錯誤,然而我應當盡力編寫在給定充足資源(分析程序,運行時檢查,機器資源,時間等)的情況下原則上可以被檢查的代碼。
糟糕的例子
// 分開編譯,可能會動態加載
extern void f(int* p);
void g(int n)
{
// 糟糕: 元素的數量沒有傳遞給 f()
f(new int[n]);
}
在這裡,元素的數量這一個關鍵信息被徹底“模糊”了,以至於靜態分析可能變得不再可行,當“f()”是ABI的一部分時可能會讓動態檢查非常困難,以至於不能“檢測”這個指針。我在免費存儲中嵌入有用的信息,但這需要對系統進行全局更改,也許還需要對編譯器進行更改,這樣的設計使得錯誤檢測非常困難。
糟糕的例子
當然,我們也可以將無數的數據隨指針一起傳進去:
// separately compiled, possibly dynamically loaded
extern void f2(int* p, int n);
void g2(int n)
{
f2(new int[n], m); // 糟糕:可能會將錯誤的無數數量傳給f()
}
將元素的數量作為參數傳遞要比僅僅傳遞指針並依賴某種(未聲明的)約定來了解或發現元素的數量更好(而且更常見)。然而(如所示),一個簡單的拼寫錯誤可能會導致嚴重的錯誤。f2()的兩個參數之間的聯繫是習慣性的,而不是顯式的。
同樣,它也暗示著f2()支持delete它的參數(或者,這是調用者犯的第二個錯誤?)。
糟糕的例子
標準庫的資源管理指針指向對象時也無法傳遞大小:
// 分開編譯,可能動態加載
// NB: 假設這裡的調用代碼是 ABI-兼容的, 使用兼容的C++編譯器以及相同的stdlib實現
extern void f3(unique_ptr, int n);
void g3(int n)
{
f3(make_unique(n), m); // 糟糕: 分開傳遞所有權和大小
}
Example
我們需要將指針和元素個數作為一個整體對象進行傳遞:
extern void f4(vector&); // 分開編譯,可能動態加載
extern void f4(span); // 分開編譯,可能動態加載
// NB: 假設這裡的調用代碼是 ABI-兼容的, 使用兼容的C++編譯器以及相同的stdlib實現
void g3(int n)
{
vectorv(n);
f4(v); // 傳遞一個引用,保留所有權(ownership)
f4(span{v}); // 傳遞一個視圖(view),保留所有權
}
這種設計將元素的數量作為對象的一個組成部分,這樣錯誤就不太可能發生,如果條件允許,動態(運行時)檢查也是可行的。
Example
我們如何同時傳遞所有權和驗證所需的所有信息?
vectorf5(int n) // OK: move
{
vectorv(n);
// ... initialize v ...
return v;
}
unique_ptrf6(int n) // 糟糕: 丟失了`n`
{
auto p = make_unique(n);
// ... initialize *p ...
return p;
}
ownerf7(int n) // 糟糕: 丟失了`n`,並且可能會忘記`delete`
{
ownerp = new int[n];
// ... initialize *p ...
return p;
}
閱讀更多 Real遊戲引擎開發者 的文章