C++ 沒有定義 non-local static 物件在不同編譯單元 (translation unit) 之間初始化的順序, 所以要極力避免 non-local static 物件相互間的存取。
今天試用 clang 編譯程式, 發覺它有個不錯的選項: -Wglobal-constructors。用這選項編譯, 遇到有 non-local static 物件會依賴其它函式 (包含 constructor) 設值時, 會輸出 warning。
這裡引用 Address Sanitizer 提供的例子:
$ cat a.cc #include <stdio.h> extern int extern_global; static int __attribute__((noinline)) read_extern_global() { return extern_global; } int x = read_extern_global() + 1; int main() { printf("%d\n", x); return 0; } $ cat b.cc int foo(); int foo() { return 42; } int extern_global = foo(); $ clang++ a.cc b.cc && ./a.out 1 $ clang++ b.cc a.cc && ./a.out 43 $ clang++ a.cc b.cc -Weverything a.cc:6:5: warning: declaration requires a global constructor [-Wglobal-constructors] int x = read_extern_global() + 1; ^ ~~~~~~~~~~~~~~~~~~~~~~~~ 1 warning generated. b.cc:3:5: warning: declaration requires a global constructor [-Wglobal-constructors] int extern_global = foo(); ^ ~~~~~ 1 warning generated.
由上可知, 編譯 a.cc 和 b.cc 的順序不同, 輸出的結果不同。加上 -Weverything 後, -Wglobal-constructors 有在第一時間抓出有問題的部份。附帶一提, clang 的錯誤訊息不止有標示錯誤的位置, 而且還是彩色的!
解決這個 warning 的方法, 和 Effective C++ Item 4 的說法一樣, 就是改用函式傳回 local static method, 這樣就會依執行的順序在執行期間初始化。不過實際情況稍微複雜了一點, 後述。
clang 另有一個參數 -Wexit-time-destructors, 會找出在結束程式時執行 destructor 的物件。雖然 destructor 執行的順序有明確的定義 (和初始化的順序相反), 不過開發者八成沒有考慮週全, 很容易在一連串 destructor 執行中用到已執行完 destructor 的物件。這個問題和 non-local static 物件初始化一樣棘手。
clang 的解決方案一樣單純: 「本來無一物, 何處惹塵埃。」統統不準用, 就不會出亂子。
$ cat p.cc struct Point { Point() {} ~Point() {} int x, y; }; const struct Point& center(); const struct Point& center() { static Point s_center; return s_center; } $ clang++ p.cc -c -Weverything p.cc:12:16: warning: declaration requires an exit-time destructor [-Wexit-time-destructors] static Point s_center; ^ 1 warning generated.
那要怎麼解決這個 warning 呢? 就是 new 一個物件, 並且不要釋放它。若懶得修改已經存取它的程式, 可以這麼做:
const struct Point& center() { static Point& s_center = *new Point(); return s_center; }
在這兩個 warning 的夾擊下會少掉很多難以察覺的錯誤, 不過寫程式時也會有一點點不便。比方說需要用到常數字串時, 不能直接寫
const std::string kMyString = "...";
得改用
const char* kMyString = "...";
對於用 std::string 做為函式參數或 STL container 的物件, 得付出一點生成 std::string 的成本。
沒有留言:
張貼留言