Programming Field - プログラミング Tips

vftableについて

vftableとは、オブジェクト指向のプログラムにおいて「仮想関数」を扱う上で重要な「モノ」です (vftableの「v」は「virtual」(仮想的な)、「f」は「function」の頭文字です)。vftableは日本語では「仮想関数テーブル」と呼ばれます。

このvftableは、C++のプログラミングにおいてクラスのメソッドを作成する際、「virtual」キーワードをつけて宣言する(仮想メソッド/仮想関数)と自動的に作成されます。vftableは「関数ポインタの配列」(のポインタ)となっており、仮想メソッドの実際のアドレスがここに順番に格納されます。

[C++]

class CTest {
public:
    virtual void Test1();
    virtual int Test2(int count);
    unsigned int Test3();
};

CTest testClass;

例えばこの「testClass変数」では、変数用のメモリ上の一番初めにvftableの配列が入り、その中身は以下のようになります。

  testClass.vftable = {...}
    [0] = &CTest::Test1
    [1] = &CTest::Test2

さらに、CTestを継承したクラスで仮想メソッドを定義すると、vftableの中身も変わってきます。

[C++]

class CTest2 : public CTest {
public:
    virtual void Test1();
    unsigned long Test4();
    virtual unsigned long Test5();
};

CTest2 testClass2;
CTest* pTest = &testClass2;
  pTest->vftable = {...} [testClass2.vftable]
    [0] = &CTest2::Test1
    [1] = &CTest::Test2
    [2] = &CTest2::Test5

仮想メソッドを継承すると、vftableのそれに当たる部分のアドレスが継承したメソッドのものに置き換えられます。仮想メソッドを呼び出す際は、例えば pTest->Test1() と呼び出すとすると、直接「CTest::Test1()」が呼び出されるのではなく、vftable配列のエントリ0に格納されているアドレスのメソッドが呼び出されます。

[C++]

CTest2 testClass2;
CTest* pTest = &testClass2;

// 以下のコードはどちらも CTest2::Test1 を呼び出す
testClass2.Test1();
pTest->Test1();

ここまでがC++での簡単なvftableのお話です。ここからCOM、Visual Basicに絡めた話になります。

宣言を細かく見れば分かるかもしれませんが、COMのインターフェイスも全て仮想メソッド(仮想関数)を使ってコードのやり取りを行っています。

例えば、PlatformSDK付属のインクルードファイル「unknwn.h」に定義されているIUnknownインターフェイスは以下の宣言がなされています。

[C/C++]

#if defined(__cplusplus) && !defined(CINTERFACE)

    interface IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE QueryInterface( 
            REFIID riid, void** ppvObject) = 0;
        virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
        virtual ULONG STDMETHODCALLTYPE Release() = 0;
    };

#else

    typedef struct IUnknownVtbl
    {
        HRESULT (STDMETHODCALLTYPE* QueryInterface)( 
            IUnknown* This, REFIID riid, void** ppvObject);
        ULONG (STDMETHODCALLTYPE* AddRef)(
            IUnknown* This);
        ULONG (STDMETHODCALLTYPE* Release)( 
            IUnknown* This);
    } IUnknownVtbl;

    interface IUnknown
    {
        struct IUnknownVtbl* lpVtbl;
    };

#ifdef COBJMACROS
#define IUnknown_QueryInterface(This,riid,ppv 	¥
    (This)->lpVtbl->QueryInterface(This,riid,ppvObject)
#define IUnknown_AddRef(This) ¥
    (This)->lpVtbl->AddRef(This)
#define IUnknown_Release(This) ¥
    (This)->lpVtbl->Release(This)
#endif /* COBJMACROS */

#endif

(一部コードを省略している部分があります。)

このコードにおいて一番外側の#if文は、「C++のスタイルを用いる」かどうかの条件文です。C++スタイルの場合は「pUnk->AddRef()」のような呼び出しを行うことが出来ます。C++スタイルでない場合は、「pUnk->lpVtbl->AddRef()」という呼び出しになります。実はこの「lpVtbl」が、まさしくvftableの配列になっているのです。なので、以下の2つは実質的に同じです。

C++スタイル

  pUnk->vftable = {...}
    [0] = (QueryInterface)
    [1] = (AddRef)
    [2] = (Release)

Cスタイル

  pUnk->lpVtbl = [struct IUnknownVtbl*]
    ->QueryInterface = (QueryInterface) [ptr]
    ->AddRef = (AddRef) [ptr]
    ->Release = (Release) [ptr]

vftableのメモリ上の配置などは、Cスタイルを見ると分かりやすいと思います。

さらにこれを応用すると、Visual Basic上でvftableをいじってしまうことも可能です。Visual Basicで作成できる「クラス」は、必ずIUnknownとIDispatchを継承しているので、これに合わせてvftableも定義されています。クラスのポインタとしてのアドレスはVBA.ObjPtrメソッドで取得できるので、CopyMemory関数によりvftableのアドレス、さらに各メソッドのアドレスまで取得できます。

最終更新日: 2008/04/08