Chapter 6.
参照カウンタと寿命


引き続き、COMのメカニズムを解説していきます。

前回のサンプルでは、オブジェクトの実行自要求を処理できるようになりました。
しかし、まだ問題があります。それはコンポーネントのライフサイクル(=寿命)を管理できていないということです。

前回のサンプルのような単純な使用法ならば、それほど問題とはなりませんが、Release関数を呼び出すタイミングをプログラマが判断できない状況においては、この実装方法では対処できません。
具体的にそういった状況のサンプルを示します。

そういった状況のソース
interface.hとcomponent.cppは前回と同じ物を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <objbase.h>
#include <iostream.h>
#include "interface.h"

class ClassA
{
public:
    ClassA()
    {
        pIChap5_1 = NULL;
    }
    ~ClassA()
    {
        pIChap5_1->FuncA();
    }

    IChap5_1    *pIChap5_1;
};

class ClassB
{
public:
    ClassB()
    {
        pIChap5_2 = NULL;
    }
    ~ClassB()
    {
        pIChap5_2->FuncB();
        pIChap5_2->Release();
    }

    IChap5_2    *pIChap5_2;
};

ClassA  a;
ClassB  b;

int main(void)
{
    IUnknown    *pUnk;

    CreateInstance(&pUnk);

    pUnk->QueryInterface(IID_IChap5_1, (void **)&a.pIChap5_1);
    pUnk->QueryInterface(IID_IChap5_2, (void **)&b.pIChap5_2);

    return 0;
}

クラスのグローバルなインスタンスのデストラクタが呼び出されるタイミングはコンパイラに依存しています。この例の場合、コンポーネントが開放された後に、そのインターフェースを使用しようとする状況が生まれる可能性があることになります。(実際に、このコードをコンパイルして実行した場合、プログラムが正常に終了するコンパイラも存在すると思われます。)



この問題を解決するために用意されている仕組みが、この章のタイトルでもある参照カウンタです。
これは文字通りコンポーネントが参照されている回数を管理する仕組みです。

参照カウンタは、コンポーネントの新しいインスタンスを作成した時点(CreateInstance関数呼び出し時)で1に初期化されています。 新しい参照を取得する毎にインクリメントし、参照を解放する毎にデクリメントする必要があります。 このインクリメントデクリメント操作に使われる関数が、それぞれAddRefRelease関数なのです。
最終的に、コンポーネントは参照カウンタが、0になった時点で自身を破棄することが可能となります。逆に参照カウンタが0より大きい場合は、何らかの参照が行われており、まだ、破棄してはならないのです。

なお、このインクリメントおよびデクリメントを行うのは、すべてクライアントの責任です。
ただし、1つ例外があり、QueryInterface関数は正常に終了した場合、内部でAddRef関数を呼び出すことが義務付けられており、QueryInterface関数の呼び出しによって取得したインターフェースに対してさらにAddRef関数を呼び出す必要はありません。

上に書いたことを踏まえてChapter 5.のソースに修正を加えたのが次のソースです。サイズの都合から別ページに掲載します。
interface.h component.cpp client.cpp



interface.hには、ほとんど変更がありません。
インターフェース名をIChap5_?からIChap6_?に変更しただけです。



component.cppを見ていきます。

クラス定義での最も大きな変更点は、参照カウンタが追加(28行目)されたことです。
また、参照カウンタを初期化するために、コンストラクタが追加(13行目)されています。
クラスの実装部分では、コンストラクタの実装(32-38行目)QueryInterface関数の修正(59行目)AddRef関数の修正(67-68行目)Release関数の修正(75-79行目)が行われています。

コンストラクタでは、参照カウンタを0に初期化しています。ここで設定するのに都合のいい値はコンポーネントの実装の仕方によって変わるので、後のサンプルでは初期化する値が変わってきます。(理由は後述)

QueryInterface関数は、COMの仕様に従って、内部でAddRef関数を呼び出すように修正されています。
AddRef関数は参照カウンタをインクリメントするように修正されています。
Release関数は参照カウンタの値をデクリメントするように修正されています。参照カウンタが0になった場合、すべての参照が解放されたと判断できるので、deleteを呼び出しています。
なお、COMの仕様ではAddRef関数とRelease関数の戻り値に依存したプログラミングは許されておらず、これら2つの関数は何を返却してもいいことになっています。実際にはデバッグのため、この例のように参照カウンタの現在値を返却することが多いようです。

CreateInstance関数は特に何も修正していません。
しかし、COMの仕様に従えば、この関数が戻った時に作成されたコンポーネントの参照カウンタは1に初期化されている必要があります。そのため、参照カウンタをコンストラクタで0に初期化し、QueryInterface関数を呼び出すことによって1にしています。
実際にコンポーネントを作成する場合は、こういった初期化の方法は取らないことの方が多くなります。(MFCおよびATLがそのような実装になっていないため)



最後にclient.cppを見ていきます。
これは駄目なサンプルを正しく動作するように修正したものです。実際にこういう使い方をすることは、まず無いと言っておきます。理由は(忘れてなければ)後の章で説明します。

修正点としては、取得した参照を解放するためにRelease関数を呼び出すようにしたところです。
48行目CreateInstance関数によって取得された参照を61行目で開放
53行目QueryInterface関数によって取得された参照を16行目で開放
57行目QueryInterface関数によって取得された参照を33行目で開放
取得した参照を開放することは、クライアントの責任であるため、それを正しく行っているだけに過ぎません。

2つのデストラクタのうち、どちらが先に実行されるかは、コンパイラに依存しますが、参照カウンタを用いれば、それに煩わされることが無いと言えます。



まとめとして、コンポーネントは参照カウンタを通じて、そのライフサイクルを決定します。
そして、その参照カウンタを操作するのはクライアントであり、AddRef(または、QueryInterface)関数より1多い回数(CreateInstance関数の対になる呼び出しを含む)Release関数を呼び出さなければなりません。
これによって、コンポーネントの寿命を適切に管理することが出来ます。

これでIUnknownの全ての役割を解説したことになります。
次の章では、実際にIUnknownを含んだCOMオブジェクト(コンポーネントと呼ぶには程遠い)を使用した実践的サンプルを解説したいと思います。

上記3ファイルを固めたサンプルです。コンソールアプリケーションとして、ビルド&実行してください。
C++用ソース:chap6.lzh(3KB未満)


前へ 次へ
COM研究室へ
総合トップへ