PMPの流儀

我が家の流儀

我が家で気になったことを夫婦で取り上げていきます

MENU

C++独立性の高いクラス設計(インターフェース)

C++C言語オブジェクト指向を無理やり付け加えたため落とし穴が至る所にある言語です。だからといって駆逐されてはおらず、速度を求めたり軽くする個所に必要です。
C++の問題点の認識とそれを解決するクラス設計を紹介します。

C++のクラス設計での問題点

問題点は2つあります。

  • クラス利用者にクラスの中(ヘッダ)が丸見え。
  • コール側がオブジェクトに必要な領域を確保する。

クラスをライブラリで提供する場合には致命的で使い物になりません。簡単な例を見てみましょう。

ヘッダ test.h

class Test
{
 int x;
public:
 Test();
 int fnc(int c);
};

ライブラリ実装 test.cpp

Test::Test() { x = 150; }
int Test::fnc(int c) { return c * x; }

コール側 main.cpp

main()
{
  Test t;
  int z = t.fnc( 30 );
}

どうでしょうか。普通のC++ですね。
問題はその後です・・・ライブラリ設計者が内部で使用する変数や追加した場合はどうなるでしょう ?

class Test
 int x;
 int y;  // 追加した変数(非公開)
public:
 Test();
 int fnc(int c);
};

int y が追加されましたが、ライブラリのコール側にどういう影響があるか考えてみます。
コール側が Test t; と宣言した時点で、Testクラスに必要な領域がスタック上から確保されます。int y; の行が追加されたことにより、確保する領域のサイズが4バイト増えます。つまり、コール側が古いヘッダを使用していた場合、yの分の領域が確保されず、メモリ破壊が起きる原因となります。

現場からこんな声が聞こえてきます

アプリ担当者 「DLLを最新にしたらぶっ飛んだ。なんなんだこれ。 」
ライブラリ担当者「連絡しましたよ。内部的なロジックを変えたので再コンパイルが必要ですって」
アプリ担当者「そんなの知らんよ。ライブラリの中の事でこっちを巻き込まないでくれよ」
・・・

こんなことなら、C言語の世界で外部公開用の構造体と専用の関数を公開する方がましです。

改善方法

C++のクラスをもっとエレガントな方法で構築する方法があります。
マイクロソフトが1990年代にCOM(Component Object Model)という仕組みを作りました。今でも、OS周りはCOMで実装されている部分が多いです。例えばDirectX やOffice はCOMで構成されています。COMはレジストリにクラスを登録する事が必要となります。OSを拡張するシステムライブラリ的な用途には向いていますが、それ以外のソフトウェアのために利用する事はまず見かけません。
今回は、COMの概念をレジストリを使わないシンプルな形で実装する例を紹介します。

改善ポイント
* 公開する関数のみを純粋仮想関数として抽象クラスを公開する。
* オブジェクトの生成・開放はライブラリ側(クラスの実装側)で行う

抽象クラスがインターフェースです。実装側は抽象クラスを派生して実装クラスを作成します。

早速先ほどのサンプルをインターフェース化してみます。

ヘッダ test.h

#define _interface struct
#define IID_TEST_0   0         // Testインターフェースの識別子

// Test インターフェース定義
_interface ITest
{
  virtual int fnc( int c ) = 0;   // インターフェースには公開する関数のみを定義
  virtual void Release() = 0;  // オブジェクトの解放
};

インターフェースで公開する関数は virtual と = 0 をつけて、純粋仮想関数として定義します。

ライブラリ側実装 test.cpp

class Test : public ITest
{
  int x;
public:
  Test() { x = 150; }

  // interface method
  int fnc(int c) { return x * c; }
  void Release() { delete this; }  // ライブラリ側でオブジェクトを開放する
};

// オブジェクト生成関数
void* CreateObject(int iid)
{
 switch(iid) {
 case IID_TEST_0:
   return new CTest();   // ライブラリ側でオブジェクトを生成する
 }
 return NULL;
}

コール側: main.cpp

main()
{
  ITest* obj = (ITest*)CreateObject( IID_TEST_0 );   // オブジェクトの生成
  int z = obj->fnc(30);                                 // 関数コール
  obj->Release();                                         // 解放
}

オブジェクトを生成するのはコール側ではなく、ライブラリ側でやるのがポイントです。これによりライブラリにとって必要な領域を確実に確保することができます。
先ほどと同じように実装クラス側で内部で使用するメンバー変数を増やしたとしても、インターフェースに変更は入らずコール側は修正やリコンパイルをする必要がないことに着目してください。

実装側: test.cpp

class Test : public ITest
{
  int x;
  int z;   // 追加
public:
  Test() { x = 150; z = 60; }

  int fnc(int c) { return x * c + z; }    // interface実装
  void Release() { delete this; }  // ライブラリ側でオブジェクトを開放する
};

実装側でどんなに複雑な仕組みになっているかは知らないけど、インターフェースだけ守ってくれれば使う側は良いのです。考えてみると当たり前のことです。当たり前のことが工夫しないとできないのがC++の大きな欠点です。
では全てのクラスをこの作り方にすべきかというそうではありません。ライブラリなど異なるモジュール間で公開するクラスをこの方式にし、同一モジュール内で使用するクラスは従来通りの使い方で良いです。

まとめ

  • モジュール間のクラス公開はインターフェースを使用する。
  • モジュール内は今まで通りのやり方で良い。

マイクロソフトはCOMによりC++をまともなクラスとして使おうとしましたが記述が大変になるのは否めません。それが後のC#へと生かされていきます。C#ではこのようなややこしい仕組みを考えずに普通にクラス設計しても独立性が高いクラス設計が可能になっています。