2005-08-08
今回は、関数の中で宣言された、ローカルな静的記憶域期間を持つオブジェクトについてお話したいと思います。関数の中で静的変数を宣言するというのはCでもありましたから、どんなものかについての説明は省略して、いきなり本題に入ります。
C++のクラス型にはコンストラクタとデストラクタがありますから、ローカルな静的オブジェクトの場合でも、当然それらが呼び出されます。クラス型でなくても、初期化時に関数を呼び出すような場合もあります。注意しないといけないのは、それらが呼び出されるタイミングです。
ローカルな静的オブジェクトであっても、C互換型(POD
*1 type)で、かつ初期値がない場合(すなわちゼロで初期化される)や定数値で初期化される場合は、Cの場合と同じで、プログラムの起動時にオブジェクトが構築されます。問題はそれ以外の場合です。
コンストラクタの呼び出しや、定数値以外(関数の呼び出しなど)での初期化が行われる場合、そのオブジェクトの構築は、最初にプログラムの実行パスがその部分に差し掛かった時点で行われます。すなわち、起動の時点ではオブジェクトは構築されないわけです。
また、デストラクタが呼び出されるのは、実際にそのオブジェクトが構築されている場合だけで、その順番は、非ローカルなオブジェクトも含めて、動的初期化が行われたときの逆順になります。
*2インライン関数の中で宣言された静的オブジェクトは、翻訳単位が変わっても同じ実体を参照することになるわけです
*3が、この場合には、実際にオブジェクトの構築が行われた翻訳単位で、解体も行われることになります。
なお、既に解体されてしまった静的オブジェクトが、再びそれが宣言されている関数が呼び出されることで、再度構築されるようなことになった場合、動作は未定義になってしまいます。通常、おそらく再構築はうまくいくでしょうが、再解体はまず行われません。いずれにしても未定義ですので、その振る舞いに依存したプログラミングは避けるべきです。
さて、オブジェクトの構築と解体の順序についてはこんなところですが、厄介なのはマルチタスク環境の場合です。私が知る限りのC++処理系では、ローカルな静的オブジェクトの構築・解体に関して、再入可能な実装になっているものはありません。
再入可能になっていない以上、あるタスクでローカルな静的オブジェクトが宣言されている部分に実行パスが差し掛かったときにタスク切り替えが起こり、別のタスクでも同じ部分に実行パスが差し掛かると、二重に構築されたり、構築されなかったり、もっと中途半端な状態になったりといったことが起こります。
こうした問題を回避するには、セマフォやミューテックス等で排他制御を行う必要があります。しかし、そうした排他制御が毎回実行されるのはパフォーマンス上の問題もあります。そこで、普通はDouble-checked lockingと呼ばれる手法を用います。
A& func()
{
static A* p = 0;
if (p == 0)
{
loc_mtx(MTXID);
if (p == 0)
{
static A a;
init = true;
p = &a;
}
unl_mtx(MTXID);
}
return *p;
}
このように、一旦フラグ変数(ここではpが0かどうかで代用)で初期化済みかどうかを確認してからミューテックス等で排他を掛け、再びフラグ変数を調べます。初期化が完了した後は、排他制御が行われることはないため、パフォーマンスの低下が最小限で済みます。
上記のコードは、Double-checked lockingに焦点を当てるために意図的に簡略化してありますが、現実には、このコードには一部問題があります。それは、クラスAのコンストラクタが例外を送出したとき、ミューテックスのロック解除が行われなくなってしまうからです。実際の使用にあたっては、その辺も考慮に入れる必要があります。
*1 Plain Old Data
*2 配列型やクラス型のオブジェクトの部分オブジェクトの構築に関わった場合は、この限りではありません。詳しくはJIS X3014:2003 3.6.3を参照のこと
*3 古い処理系や標準準拠度の低い処理系では、こうなっていない場合がある
Comment