destroy
とscoped
オブジェクトのライフタイムについては、ライフタイムと基本操作の章で説明した。
後の章で、オブジェクトはコンストラクタ(this()
)で使用準備され、オブジェクトの最終操作はデストラクタ(~this()
)で実行されることを学んだ。
構造体やその他の値型の場合、デストラクタはオブジェクトのライフタイムが終了した時点で実行される。クラスやその他の参照型の場合、デストラクタは将来のある時点でガベージコレクタによって実行される。重要な違いは、クラスオブジェクトのデストラクタは、そのライフタイムが終了しても実行されないことだ。
システムリソースは通常、デストラクタでシステムに返される。例えば、std.stdio.File
は、そのデストラクタでファイルリソースをオペレーティングシステムに返す。クラスオブジェクトのデストラクタがいつ呼び出されるかは定かではないため、そのオブジェクトが保持しているシステムリソースは、他のオブジェクトが同じリソースを取得できなくなるまで返されない可能性がある。
デストラクタを遅く呼び出す例
クラスデストラクタを遅く実行した場合の影響を確認するために、クラスを定義しよう。次のコンストラクタは、static
カウンタを加算し、デストラクタはそれを減算する。覚えているように、static
メンバーは1つしかなく、その型を持つすべてのオブジェクトで共有される。このようなカウンタは、まだ破棄されていないオブジェクトの数を示すことになる。
次のプログラムは、ループ内でそのクラスのオブジェクトを構築する。
各LifetimeObserved
オブジェクトの寿命は、実際には非常に短い。その寿命は、new
キーワードによって構築された時点で始まり、foreach
ループの閉じ中括弧で終わる。その後、各オブジェクトはガベージコレクタの責任となる。開始コメントと 終了コメントは、寿命の開始と終了を示している。
特定の時点で1つのオブジェクトが存続していても、カウンタの値は、寿命が終了してもデストラクタが実行されないことを示している。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 2 | 3 | 4 | 5 | 6 |
この出力によると、ガベージコレクタのメモリスイープアルゴリズムは、最大8個のオブジェクトのデストラクタの実行を遅らせている。(注釈: 出力は、ガベージコレクションアルゴリズム、使用可能なメモリ、その他の要因によって異なる場合がある。)
destroy()
デストラクタを実行する
destroy()
オブジェクトのデストラクタを実行する:
前述と同様に、new
の結果、コンストラクタによってLifetimeObserved.counter
の値が1増加し、1になる。今回は、出力された直後に、destroy()
によってオブジェクトのデストラクタが実行され、カウンタの値が再び0に減少する。そのため、今回はその値は常に1になる。
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
オブジェクトが破棄されると、そのオブジェクトは不正な状態にあるものと見なされ、以降使用してはならない:
destroy()
は主に参照型用だが、struct
オブジェクトに対しても呼び出して、通常の有効期間が終了する前に破棄することができる。
使用タイミング
前の例で見たように、destroy()
は、ガベージコレクタに依存せずに、特定のタイミングでリソースを解放する必要がある場合に使用する。
例
コンストラクタおよびその他の特殊関数の章で、XmlElement
構造体を設計した。この構造体は、<tag>value</tag>
の形式でXML要素を出力するために使用されていた。終了タグの出力は、デストラクタの責任だった。
この構造体を使用したプログラムでは、次のような出力が生成された。今回は、class
キーワードと混同しないように、"class"という単語を"course"に置き換えている。
<courses>
<course0>
<grade>
72
</grade> ← 終了タグは正しい行に表示されている
<grade>
97
</grade> ←
<grade>
90
</grade> ←
</course0> ←
<course1>
<grade>
77
</grade> ←
<grade>
87
</grade> ←
<grade>
56
</grade> ←
</course1> ←
</courses> ←
以前の出力は、XmlElement
がstruct
であるため、偶然にも正しい。望ましい出力は、オブジェクトを適切なスコープに配置するだけで実現できる:
デストラクタは、オブジェクトが破棄される際に終了タグを出力する。
クラスの動作の違いを確認するため、XmlElement
をクラスに変換しよう:
デストラクタの呼び出し責任がガベージコレクタに委ねられたため、プログラムは希望する出力を生成しない:
<courses>
<course0>
<grade>
57
<grade>
98
<grade>
87
<course1>
<grade>
84
<grade>
60
<grade>
99
</grade> ← 終了タグは最後に表示される
</grade> ←
</grade> ←
</course1> ←
</grade> ←
</grade> ←
</grade> ←
</course0> ←
</courses> ←
デストラクタは、すべてのオブジェクトに対して実行されるが、今回はプログラムの終了時に実行される。(注釈: ガベージコレクタは、すべてのオブジェクトに対してデストラクタが呼び出されることを保証するものではない。実際には、終了タグがまったく出力されない場合もある。)
destroy()
デストラクタがプログラムの意図したポイントで呼び出されることを保証する:
これらの変更により、コードの出力は、構造体を使用するコードの出力と一致するようになった。
<courses>
<course0>
<grade>
66
</grade> ← 終了タグは正しい行に表示されている
<grade>
75
</grade> ←
<grade>
68
</grade> ←
</course0> ←
<course1>
<grade>
73
</grade> ←
<grade>
62
</grade> ←
<grade>
100
</grade> ←
</course1> ←
</courses> ←
scoped()
デストラクタを自動的に呼び出す
上記のプログラムには弱点がある。destroy()
行が実行される前に、通常は例外がスローされることで、スコープが終了してしまう可能性がある。例外がスローされた場合でもdestroy()
行を実行する必要がある場合は、例外の章で説明したscope()
などの機能を利用するとよい。
別の解決策は、new
キーワードではなくstd.typecons.scoped
を使用してクラスオブジェクトを構築することだ。scoped()
はクラスオブジェクトをstruct
でラップし、そのstruct
オブジェクトのデストラクタは、自身スコープ外に出た際にクラスオブジェクトを破棄する。
scoped()
の効果は、クラスオブジェクトの寿命に関して、構造体オブジェクトと同様に動作させることである。
以下の変更を加えると、プログラムは以前と同じ期待通りの出力を生成する:
destroy()
行がなくなったことに注意。
scoped()
は、実際のclass
オブジェクトをカプセル化した特別なstruct
オブジェクトを返す関数だ。返されたオブジェクトは、カプセル化されたオブジェクトのプロキシとして機能する。(実際、上記のcourses
の型はXmlElement
ではなく、Scoped
だ。)
struct
オブジェクトのデストラクタが、そのライフタイム終了時に自動的に呼び出されると、カプセル化されているclass
オブジェクトに対してdestroy()
を呼び出す。(これは、リソース取得は初期化(RAII)イディオムの適用例だ。scoped()
は、テンプレートとalias this
の両方を使用して実現している。これらは後述する章で詳しく説明する。
プロキシオブジェクトは、できるだけ便利に使用できることが望ましい。実際、scoped()
が返すオブジェクトは、実際のclass
型とまったく同じように使用できる。例えば、実際の型のメンバー関数をそのオブジェクトで呼び出すことができる。
ただし、この利便性には代償が伴う。プロキシオブジェクトは、実際のオブジェクトを破壊する直前に、そのオブジェクトへの参照を渡してしまう可能性がある。これは、実際のclass
型が左側に明示的に指定されている場合に発生する。
この定義では、c
はプロキシオブジェクトではなく、プログラマによって定義された、カプセル化されたオブジェクトを参照するclass
変数だ。残念ながら、右辺で構築されるプロキシオブジェクトは、それを構築する式の最後に終了する。その結果、プログラムでc
を使用するとエラーになり、おそらく実行時エラーが発生する。
セグメンテーションフォールト
そのため、scoped()
変数を実際の型で定義しないでほしい。
要約
destroy()
は、クラスオブジェクトのデストラクタを明示的に実行するために使用される。scoped()
で構築されたオブジェクトは、それぞれのスコープを離れる際に破棄される。scoped()
変数を実際の型で定義するのはバグである。