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 的成本。