...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

D言語でMicrosoft Component Object Model (COM) を使う (その14)

D言語 Advent Calendar 2025 18日目の記事です。

その1でCOMインタフェースを使うために必要な宣言や定義の準備ができて、 その2でCOMの初期化と解放ができるようになりました。 その3では実際にファイルを開くダイアログを表示することができ、その4では選択したファイルのパスを取得できました。 その5ではQueryInterfaceを使って別のCOMインタフェースを取得して、ファイル作成日時を取得できました。 その6ではCOMインタフェースをアップキャストもダウンキャストもできないことを説明しました。

その7ではCOMオブジェクトとCOMインタフェースを自作するための準備をしました。 その8とその9では自作COMファクトリをCOMサーバーとして登録し、 自作COMオブジェクトと自作COMインタフェースを作成できました。 その10では既存のCOMインタフェースを拡張した新しいCOMインタフェースとCOMオブジェクトを作成しました。 その11では、COMファクトリクラスを拡張して、新しいCOMインタフェースを使えました。

その12では、AddRefメソッドとReleaseメソッドを自分で呼ばなくていいように ComPtrというスマートポインタを作ることにしましたが、 その13では構築と破棄を同じ回数捉えることができませんでした。

構造体のコピーコンストラクタ

Structs, Unions - D Programming Language

改めて構造体のコピーコンストラクタについて調べてみると、 postblitはコピーコンストラクタに置き換えられることになっています。 定義するのはコピーコンストラクタだけでいいのかもしれません。 あるいは、代入演算子オーバーロード(opAssgn)と組み合わせるのかもしれません。

また、コピーコンストラクタのシグネチャは、 Type(ref Type args)のように第一引数がrefである必要があります。 前回はType(Type args)だったので、コピーコンストラクタじゃなくて Type引数を受け取る普通のコンストラクタだったようです。

コピーコンストラクタだけで参照カウント式スマートポインタを書いてみる

試行錯誤したところ、次のコードに行き着きました。

まず、管理される側のオブジェクトとしてComLikeObjectを作りました。 参照カウントを加算するincメソッドと、参照カウントを減算するdecメソッドを持った シンプルなクラスを定義して、挙動を追いかけられるようにしています。

class ComLikeObject
{
    this(int id)
    {
        writeln("Create id:", id);
        this.id = id;
        inc();
    }
    final void inc()
    {
        count++; // 参照カウントを+1
    }
    final void dec()
    {
        count--; // 参照カウントを-1
        if (count == 0)
        {
            destroy(this); // 参照カウントがゼロになったら破棄
        }
    }
    ~this()
    {
        writeln("Destroy id:", id);
    }
private:
    int id; // 追跡用の識別番号
    int count; // 参照カウンタ
}

次に、実験的なスマートポインタにすべく、 SimplrPtr構造体をコピーコンストラクタだけで書きました。

struct SimplePtr
{
    // SimplePtr a = SimplePtr(new ComLikeObject);
    this(ComLikeObject param) // コンストラクタ(ctor)
    {
        obj = param;
        writeln("this(ComLikeObject param) / id:", obj.id, " / count:", obj.count);
    }
    // SimplePtr a; a = SimplePtr(new ComLikeObject);
    this(ref SimplePtr param) // コピーコンストラクタ(copyCtor)
    {
        obj = param.obj;
        obj.inc();
        writeln("copyCtor / id:", obj.id, " / count:", obj.count);
    }
    ~this() // デストラクタ
    {
        if (!obj) return;
        obj.dec();
        writeln("~this / id:", obj.id, " / count:", obj.count);
        obj = null;
    }
private:
    ComLikeObject obj; // 管理対象オブジェクト
}

使う側のコードは、次のとおり。

void f(SimplePtr x) {}

SimplePtr g(int id)
{
    return SimplePtr(new ComLikeObject(id)); // ctor
}

void main()
{
    writeln("-- Sec.1 --");
    {
        SimplePtr a = SimplePtr(new ComLikeObject(1)); // ctor
    }
    writeln("-- Sec.2 --");
    {
        SimplePtr a;
        a = SimplePtr(new ComLikeObject(2)); // ctor
    }
    writeln("-- Sec.3 --");
    {
        SimplePtr a = SimplePtr(new ComLikeObject(3)); // ctor
        f(a); // copyCtor
    }
    writeln("-- Sec.4 --");
    {
        SimplePtr a = SimplePtr(new ComLikeObject(41)); // ctor
        SimplePtr b = SimplePtr(new ComLikeObject(42)); // ctor
        a = b; // copyCtor
    }
    writeln("-- Sec.5 --");
    {
        f(SimplePtr(new ComLikeObject(5))); // ctor
    }
    writeln("-- Sec.6 --");
    {
        SimplePtr a = g(6); // ctor
    }
    writeln("-- Sec.7 --");
    {
        SimplePtr a;
        a = g(7); // ctor
    }

今回も個別に見ていきます。

セクション1

writeln("-- Sec.1 --");
{
    SimplePtr a = SimplePtr(new ComLikeObject(1)); // ctor
}
-- Sec.1 --
Create id:1
this(ComLikeObject param) / id:1 / count:1
Destroy id:1
~this / id:0 / count:0

newでオブジェクトが作成されてCreateが出ています。 コピーコンストラクタが呼ばれて参照カウントが1になっています。 ブロックの最後にaが破棄され、参照カウンタが0になったので デストラクタが呼ばれてDestroyが出ています。 最後に、~thisが出ています。

これは期待する動作をしています。

セクション2

writeln("-- Sec.2 --");
{
    SimplePtr a;
    a = SimplePtr(new ComLikeObject(2)); // ctor
}
-- Sec.2 --
Create id:2
this(ComLikeObject param) / id:2 / count:1
Destroy id:2
~this / id:0 / count:0

aの初期化をするか代入をするかが違いますが、 挙動はセクション1と全く同じでした。

これも期待する動作をしています。

セクション3

writeln("-- Sec.3 --");
{
    SimplePtr a = SimplePtr(new ComLikeObject(3)); // ctor
    f(a); // copyCtor
}
Create id:3
this(ComLikeObject param) / id:3 / count:1
copyCtor / id:3 / count:2
~this / id:3 / count:1
Destroy id:3
~this / id:0 / count:0

newでオブジェクトが作成されてCreateが出るまではセクション1、2と同じです。 今度は、f関数の引数xに対してaがコピーされるときにコピーコンストラクタが呼ばれ、参照カウントが2になります。 その後、f関数から戻るときに引数xが破棄されて~thisが呼ばれ、参照カウントが1になります。 最後にブロックを出るときにaが破棄されて参照カウントが0になり、Destroyが出ています。

これも期待どおりの挙動です。

セクション4

writeln("-- Sec.4 --");
{
    SimplePtr a = SimplePtr(new ComLikeObject(41)); // ctor
    SimplePtr b = SimplePtr(new ComLikeObject(42)); // ctor
    a = b; // copyCtor
}
-- Sec.4 --
Create id:41
this(ComLikeObject param) / id:41 / count:1
Create id:42
this(ComLikeObject param) / id:42 / count:1
copyCtor / id:42 / count:2
Destroy id:41
~this / id:0 / count:0
~this / id:42 / count:1
Destroy id:42
~this / id:0 / count:0

代入するといつも大変なことになりますね。

id 41のオブジェクトを作成してaに入れて、id 42のオブジェクトを作成したbに入れています。 2つのスマートポインタが別々のオブジェクトを管理しているので、参照カウントはどちらも1です。

次に、bをaに代入したのでid 42がid 41の方にコピーされますが、 ここでコピーコンストラクタが呼ばれ、id 42の参照カウントが増えて2になります。

この後、id 41のDestroyが出ているので、aに入っていたオブジェクトが破棄されています。 実は、代入された方のオブジェクトは、デストラクタが呼ばれるようです。 そのため、この後の参照カウンタも期待どおりに減っています。

セクション5

writeln("-- Sec.5 --");
{
    f(SimplePtr(new ComLikeObject(5))); // ctor
}
-- Sec.5 --
Create id:5
this(ComLikeObject param) / id:5 / count:1
Destroy id:5
~this / id:0 / count:0

id 5のオブジェクトが作成されてCreateが出ています。 普通のコンストラクタが呼ばれて、参照カウントが1になります。 f関数の引数xへのコピーはトラップされていませんが*1、 f関数から呼び出し元に戻ってくるときに引数xが破棄されるのでDestroyが呼ばれて 参照カウントも0になっています。

これも期待する動作をしています。

セクション6

writeln("-- Sec.6 --");
{
    SimplePtr a = g(6); // ctor
}
-- Sec.6 --
Create id:6
this(ComLikeObject param) / id:6 / count:1
Destroy id:6
~this / id:0 / count:0

g関数の中でid 6のオブジェクトが作成されてCreateが出ています。 aを構築するときに普通のコンストラクタが呼ばれて、参照カウントが1になります。 ブロックから出るときにaが破棄されるのでDestroyが出て、参照カウントが0になります。

これも期待する動作です。

セクション7

writeln("-- Sec.7 --");
{
    SimplePtr a;
    a = g(7); // ctor
}
-- Sec.7 --
Create id:7
this(ComLikeObject param) / id:7 / count:1
Destroy id:7
~this / id:0 / count:0

コードの1行目はログを出していません。 g関数の中でid 7のオブジェクトが作成されたCreateが出ています。 普通のコンストラクタが呼ばれて、参照カウントが1になります。 ブロックから出るときにaが破棄されるのでDestroyが出て、参照カウントが0になります。

これも期待する挙動をしています。

コピーコンストラクタだけでも書けそう

コピーコンストラクタだけを定義するようにしたら、 どのセクションの場合でもスマートポインタの構築と破棄の回数が同じになり、 参照カウントが0になったときにオブジェクトを削除するようになったようです。

*1:いわゆるムーブが起こっているのでしょうか。