destroyscoped

オブジェクトのライフタイムについては、ライフタイムと基本操作の章で説明した。

後の章で、オブジェクトはコンストラクタ(this())で使用準備され、オブジェクトの最終操作はデストラクタ(~this())で実行されることを学んだ。

構造体やその他の値型の場合、デストラクタはオブジェクトのライフタイムが終了した時点で実行される。クラスやその他の参照型の場合、デストラクタは将来のある時点でガベージコレクタによって実行される。重要な違いは、クラスオブジェクトのデストラクタは、そのライフタイムが終了しても実行されないことだ。

システムリソースは通常、デストラクタでシステムに返される。例えば、std.stdio.Fileは、そのデストラクタでファイルリソースをオペレーティングシステムに返す。クラスオブジェクトのデストラクタがいつ呼び出されるかは定かではないため、そのオブジェクトが保持しているシステムリソースは、他のオブジェクトが同じリソースを取得できなくなるまで返されない可能性がある。

デストラクタを遅く呼び出す例

クラスデストラクタを遅く実行した場合の影響を確認するために、クラスを定義しよう。次のコンストラクタは、staticカウンタを加算し、デストラクタはそれを減算する。覚えているように、staticメンバーは1つしかなく、その型を持つすべてのオブジェクトで共有される。このようなカウンタは、まだ破棄されていないオブジェクトの数を示すことになる。

class LifetimeObserved {
    int[] array;           // ← 各オブジェクトに属する

    static size_t counter; // ← すべてのオブジェクトで共有される

    this() {
        /* 比較的大きな配列を使用して、各オブジェクトが
         * 大量のメモリを消費するようにしている。これにより、
         * ガベージコレクタがオブジェクトのデストラクタを
         * より頻繁に呼び出し、より多くのオブジェクトのためのスペースを
         * 解放するようになることを期待している。 */
        array.length = 30_000;

        /* 構築中のこのオブジェクトのカウンターを
         * 加算する。 */
        ++counter;
    }

    ~this() {
        /* 破壊中のこのオブジェクトのカウンターを
         * 減算する。 */
        --counter;
    }
}

次のプログラムは、ループ内でそのクラスのオブジェクトを構築する。

import std.stdio;

void main() {
    foreach (i; 0 .. 20) {
        auto variable = new LifetimeObserved;  // ← 開始
        write(LifetimeObserved.counter, ' ');
    } // ← 終了

    writeln();
}

LifetimeObservedオブジェクトの寿命は、実際には非常に短い。その寿命は、newキーワードによって構築された時点で始まり、foreachループの閉じ中括弧で終わる。その後、各オブジェクトはガベージコレクタの責任となる。開始コメントと 終了コメントは、寿命の開始と終了を示している。

特定の時点で1つのオブジェクトが存続していても、カウンタの値は、寿命が終了してもデストラクタが実行されないことを示している。

12345678234567823456

この出力によると、ガベージコレクタのメモリスイープアルゴリズムは、最大8個のオブジェクトのデストラクタの実行を遅らせている。(注釈: 出力は、ガベージコレクションアルゴリズム、使用可能なメモリ、その他の要因によって異なる場合がある。)

destroy()デストラクタを実行する

destroy()オブジェクトのデストラクタを実行する:

void main() {
    foreach (i; 0 .. 20) {
        auto variable = new LifetimeObserved;
        write(LifetimeObserved.counter, ' ');
        destroy(variable);
    }

    writeln();
}

前述と同様に、newの結果、コンストラクタによってLifetimeObserved.counterの値が1増加し、1になる。今回は、出力された直後に、destroy()によってオブジェクトのデストラクタが実行され、カウンタの値が再び0に減少する。そのため、今回はその値は常に1になる。

11111111111111111111

オブジェクトが破棄されると、そのオブジェクトは不正な状態にあるものと見なされ、以降使用してはならない:

destroy(variable);
// ...
// 警告: 無効なオブジェクトを使用している可能性がある
writeln(variable.array);
D

destroy()は主に参照型用だが、structオブジェクトに対しても呼び出して、通常の有効期間が終了する前に破棄することができる。

使用タイミング

前の例で見たように、destroy()は、ガベージコレクタに依存せずに、特定のタイミングでリソースを解放する必要がある場合に使用する。

コンストラクタおよびその他の特殊関数の章で、XmlElement構造体を設計した。この構造体は、<tag>value</tag>の形式でXML要素を出力するために使用されていた。終了タグの出力は、デストラクタの責任だった。

struct XmlElement {
    // ...

    ~this() {
        writeln(indentation, "</", name, '>');
    }
}
D

この構造体を使用したプログラムでは、次のような出力が生成された。今回は、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>     

以前の出力は、XmlElementstructであるため、偶然にも正しい。望ましい出力は、オブジェクトを適切なスコープに配置するだけで実現できる:

void main() {
    const courses = XmlElement("courses", 0);

    foreach (courseId; 0 .. 2) {
        const courseTag = "course" ~ to!string(courseId);
        const courseElement = XmlElement(courseTag, 1);

        foreach (i; 0 .. 3) {
            const gradeElement = XmlElement("grade", 2);
            const randomGrade = uniform(50, 101);

            writeln(indentationString(3), randomGrade);

        } // ← gradeElementが破棄される

    } // ← courseElementが破棄される

} // ← coursesが破棄される
D

デストラクタは、オブジェクトが破棄される際に終了タグを出力する。

クラスの動作の違いを確認するため、XmlElementをクラスに変換しよう:

import std.stdio;
import std.array;
import std.random;
import std.conv;

string indentationString(int level) {
    return replicate(" ", level * 2);
}

class XmlElement {
    string name;
    string indentation;

    this(string name, int level) {
        this.name = name;
        this.indentation = indentationString(level);

        writeln(indentation, '<', name, '>');
    }

    ~this() {
        writeln(indentation, "</", name, '>');
    }
}

void main() {
    const courses = new XmlElement("courses", 0);

    foreach (courseId; 0 .. 2) {
        const courseTag = "course" ~ to!string(courseId);
        const courseElement = new XmlElement(courseTag, 1);

        foreach (i; 0 .. 3) {
            const gradeElement = new XmlElement("grade", 2);
            const randomGrade = uniform(50, 101);

            writeln(indentationString(3), randomGrade);
        }
    }
}

デストラクタの呼び出し責任がガベージコレクタに委ねられたため、プログラムは希望する出力を生成しない:

<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()デストラクタがプログラムの意図したポイントで呼び出されることを保証する:

void main() {
    const courses = new XmlElement("courses", 0);

    foreach (courseId; 0 .. 2) {
        const courseTag = "course" ~ to!string(courseId);
        const courseElement = new XmlElement(courseTag, 1);

        foreach (i; 0 .. 3) {
            const gradeElement = new XmlElement("grade", 2);
            const randomGrade = uniform(50, 101);

            writeln(indentationString(3), randomGrade);

            destroy(gradeElement);
        }

        destroy(courseElement);
    }

    destroy(courses);
}
D

これらの変更により、コードの出力は、構造体を使用するコードの出力と一致するようになった。

<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()の効果は、クラスオブジェクトの寿命に関して、構造体オブジェクトと同様に動作させることである。

以下の変更を加えると、プログラムは以前と同じ期待通りの出力を生成する:

import std.typecons;
// ...
void main() {
    const courses = scoped!XmlElement("courses", 0);

    foreach (courseId; 0 .. 2) {
        const courseTag = "course" ~ to!string(courseId);
        const courseElement = scoped!XmlElement(courseTag, 1);

        foreach (i; 0 .. 3) {
            const gradeElement = scoped!XmlElement("grade", 2);
            const randomGrade = uniform(50, 101);

            writeln(indentationString(3), randomGrade);
        }
    }
}
D

destroy()行がなくなったことに注意。

scoped()は、実際のclassオブジェクトをカプセル化した特別なstructオブジェクトを返す関数だ。返されたオブジェクトは、カプセル化されたオブジェクトのプロキシとして機能する。(実際、上記のcoursesの型はXmlElementではなく、Scopedだ。)

structオブジェクトのデストラクタが、そのライフタイム終了時に自動的に呼び出されると、カプセル化されているclassオブジェクトに対してdestroy()を呼び出す。(これは、リソース取得は初期化(RAII)イディオムの適用例だ。scoped()は、テンプレートとalias thisの両方を使用して実現している。これらは後述する章で詳しく説明する。

プロキシオブジェクトは、できるだけ便利に使用できることが望ましい。実際、scoped()が返すオブジェクトは、実際のclass型とまったく同じように使用できる。例えば、実際の型のメンバー関数をそのオブジェクトで呼び出すことができる。

import std.typecons;

class C {
    void foo() {
    }
}

void main() {
    auto p = scoped!C();
    p.foo();    // プロキシオブジェクトpが型Cとして使用されている
}

ただし、この利便性には代償が伴う。プロキシオブジェクトは、実際のオブジェクトを破壊する直前に、そのオブジェクトへの参照を渡してしまう可能性がある。これは、実際のclass型が左側に明示的に指定されている場合に発生する。

C c = scoped!C();    // ← バグ
c.foo();             // ← 破壊されたオブジェクトにアクセスする
D

この定義では、cはプロキシオブジェクトではなく、プログラマによって定義された、カプセル化されたオブジェクトを参照するclass変数だ。残念ながら、右辺で構築されるプロキシオブジェクトは、それを構築する式の最後に終了する。その結果、プログラムでcを使用するとエラーになり、おそらく実行時エラーが発生する。

セグメンテーションフォールト

そのため、scoped()変数を実際の型で定義しないでほしい。

C         a = scoped!C();    // ← バグ
auto      b = scoped!C();    // ← 正しい
const     c = scoped!C();    // ← 正しい
immutable d = scoped!C();    // ← 正しい
D
要約