static initialization order fiasco是對C++的一個非常微妙的並且常見的誤解。不幸的是,錯誤發生在main()開始之前,很難檢測到。
簡而言之,假設你有存在於不同的源檔案x.cpp 和y.cpp的兩個靜態對象x 和 y。再假定y對象的建構函式會調用x對象的某些方法。
就是這些。就這麼簡單。
結局是你完蛋不完蛋的機會是50%-50%。如果碰巧x.cpp的編輯單元先被初始化,這很好。但如果y.cpp的編輯單元先被初始化,然後y的建構函式比x的建構函式先運行。也就是說,y的建構函式會調用x對象的方法,而x對象還沒有被構造。
如何防止“static initialization order fiasco”?
使用“首次使用時構造(construct on first use)”法,意思就是簡單地將靜態對象包裹於函數內部。
例如,假設你有兩個類,Fred 和 Barney。有一個稱為x的全域Fred對象,和一個稱為y的全域Barney對象。Barney的建構函式調用了x對象的goBowling()方法。 x.cpp檔案定義了x對象:
// File x.cpp
#include "Fred.hpp"
Fred x;
y.cpp檔案定義了y對象:
// File y.cpp
#include "Barney.hpp"
Barney y;
Barney建構函式的全部看起來可能是象這樣的:
// File Barney.cpp
#include "Barney.hpp"
Barney::Barney()
{
// ...
x.goBowling();
// ...
}
正如以上所描述的,由於它們位於不同的源檔案,那麼 y 在 x 之前構造而發生災難的機率是50%。
這個問題有許多解決方案,但一個非常簡便的方案就是用一個返回Fred對象引用的全域函數x(),來取代全域的Fred對象 x。
// File x.cpp
#include "Fred.hpp"
Fred& x()
{
static Fred* ans = new Fred();
return *ans;
}
由於靜態局部對象只在控制流程第一次越過它們的聲明時構造,因此以上的new Fred()語句只會執行一次:x()被第一次調用時。每個後續的調用將返回同一個Fred對象(ans指向的那個)。然後你所要做的就是將 x 改成 x():
// File Barney.cpp
#include "Barney.hpp"
Barney::Barney()
{
// ...
x().goBowling();
// ...
}
由於該全域的Fred對象在首次使用時被構造,因此被稱為首次使用時構造法(Construct On First Use Idiom)
這種方法的不利方面是Fred對象不會被析構。C++ FAQ Book有另一種技巧消除這個影響(但面臨了“static de-initialization order fiasco”的代價)。