2014年1月20日 星期一

clang 避免 non-local static 物件初始化順序的方法

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

沒有留言:

張貼留言

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...