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++のクラスをもっとエレガントな方法で構築する方法があります。
マイクロソフトが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++の大きな欠点です。 Better C と呼ばれるわけですね。
では全てのクラスをこの作り方にすべきかというそうではありません。ライブラリなど異なるモジュール間で公開するクラスをこの方式にし、モジュール内のみで使用するクラスは従来通りの使い方で良いです。
まとめ
- モジュール間のクラス公開はインターフェースを使用する。
- モジュール内は今まで通りのやり方で良い。