ゆき社長

シーゲンガーのお勉強 ゲームプログラマ、ゲーマー、色々!

多重継承にまつわるエトセトラ

忘れないようにメモ

多重継承と言えば、Operatorと並んで

オブジェクト指向におけるGANと呼ばれたりもして

悪いイメージが多いと思うけど、そんな事はない

正しく使えば、これほど便利な物はないです

多重継承をオブジェクト指向のgotoだと言って使わない人は

単なる多重継承を理解してないからです

だって ゴクウとマジュニアから派生したゴハンは

カメハメ波だけでなく、魔貫光殺砲もどきの魔閃光を使えるんだもんね

多重継承はすばらしい フォーーーーーーー

というわけで、ログを出力するクラスの設計をしてみましょう

まず、ログといってもファイル、シリアル、TCP/UDP、メールスロット、共有ファイル、DB・・

様々な出力が想定されます

例えば、開発の時にファイルにログ出力をしていました

あなたは

class CLogFile

{

public:

  virtual bool OpenFile( LPCTSTR pszFileName );

  virtual void CloseFile();

  virtual DWORD WriteLog( LPCTSTR pszMessage );

};

とLogクラスを作ったでしょう。

しかし今度はリモートPCでのテストを行いました。

リモートPCのファイルを確認しつつ・・ 面倒ですね。なのでTCP版を作ります

class CLogTCP

{

public:

  virtual bool Connect( LPCTSTR pszIP, int nPort );

  virtual void Disconnect();

  virtual DWORD WriteLog( LPCTSTR pszMessage );

};

そして、ログを出力していた箇所を全部書き換えです。面倒ですね

しかし今度はDBに書く仕様になりました。 そしてDB用のクラスを作り

呼び出す部分を書き直して・・・

そんな事を繰り返すわけです。

賢いあなたは、ログ出力の部分を関数にして、インタフェースが変わっても

関数1個で直せるようにしました。ダサいですがましです

そんな時 賢いあなたは、インタフェースクラスを作り、ポリモーフィズムを思いつきます

class ILog

{

public:

  virtual bool open( LPCTSTR pszFileName, int nPort ) = 0;

  virtual void close() = 0;

  virtual DWORD write( LPCVOID pvData, DWORD dwLen ) = 0;

};

そして、このインタフェースを継承したログクラスを作ります

class CLogFile : public ILog

...

class CLogTCP : public ILog

...

class CLogDB : public ILog

...

そしてオブジェクト作成の部分は Fileログの場合は

ILog *g_pLog = new( CLogFile() );

これで、ポリモーフィズムにより単一なインタフェースでログが吐け、

ログの仕様が変わっても newの部分を変えるだけで終わり

完璧

しかし、今まではログ出力は文字列のみでしたが

そこにエラーレベルというintのフィールドや、エラーコード、モジュール名などのフィールドが増えました

さらに、状況によってはUnicodeで出力したい時やXML形式での出力・・・

そんな時は これですね CLogFileを派生させましょう

class CLogFileEx : public CLogFile

{

public:

  virtual DWORD writeA( LPCTSTR pszMsg, int nCode, LPCTSTR pszModule );

  virtual DWORD writeW( LPCTSTR pszMsg, int nCode, LPCTSTR pszModule );

  virtual DWORD writeXML( LPCTSTR pszMsg, int nCode, LPCTSTR pszModule );

  ...

};

しかし、 同じような派生を CLogTCPやCLogDB 等にも増やさなければいけません

少し賢いあなたは、追加分のクラスを作成し

派生の時にメンバとして持ち、Implementsすれば少しは楽になりますが

バインド部分は毎回書かねばなりません

さらに、追加されたメソッドは ILogには含まれていないので

単一なインタフェースでは使用できません

さぁ困った。結局追加仕様が来るたびにソースをいっぱい直すか

基本クラスに手を入れるか・・・

そこでおすすめなのが多重継承

まず機能単位でクラスを分けてみます

ILog ログの最低限な機能を示すインタフェース

CLogFile ファイルログ出力の最低限のクラス

CLogTCP TCPログ出力の最低限のクラス

CLogDB DBログ出力の最低限のクラス

そしてそれらに機能追加する為のクラスを設計

IWriteConverter 文字コード変更でwriteできるインタフェース

CWriteConverter その実装

IWriteConverterXML XMLでwriteできるインタフェース

CWriteConverterXML その実装

ILogFormat フォーマット付きログ出力関数郡

CLogFormat その実装

それぞれ見てみると

IWriteConverter

{

public:

  virtual DWORD write( LPCVOID pvData, DWORD dwLen ) = 0;

  virtual DWORD writeA( LPCSTR pszData ) = 0;

  virtual DWORD writeA( LPCWSTR pszData ) = 0;

  virtual DWORD writeW( LPCSTR pszData ) = 0;

  virtual DWORD writeW( LPCWSTR pszData ) = 0;

};

CWriteConverter : virtual public IWriteConverter

{

  DWORD writeA( LPCSTR pszData ){

    ((IWriteConverter*)this)->write( (LPCVOID)pszData, (DWORD)strlen(pszData) );

  };

  DWORD writeA( LPCWSTR pszData ){

   ・・・ UNICODE変換する ・・・

    ((IWriteConverter*)this)->write( (LPCVOID)pszData, (DWORD)_wcslen(pszWCS)*sizeof(UCHAR) );

  };

  ...

};

class ILogFormat : virtual public IWriteConverter

{

public:

  DWORD writeErrorA( LPCTSTR pszMsg, int nErrCode, LPCTSTR pszModule ) = 0;

  DWORD writeInfoA( LPCSTR pszMsg, int nInfoCode, LPCSTR pszModule ) = 0;

  DWORD writeErrorW( LPCTSTR pszMsg, int nErrCode, LPCTSTR pszModule ) = 0;

  DWORD writeInfoW( LPCSTR pszMsg, int nInfoCode, LPCSTR pszModule ) = 0;

  ...

};

class CLogFormat : virtual public ILogFormat

{

  DWORD writeErrorA( LPCSTR pszMsg, int nErrCode, LPCSTR pszModule )

  {

    ・・・ XMLに変換処理 ・・・

    ((IWriteConverter*)this)->writeA( pszData );

  };

  ...

};

class CLogFile : virtual public ILog

{

  virtual bool open( LPCTSTR pszFileName, int nPort ){ ほげほげ };

  virtual void close(){ ほげほげ };

};

そしていよいよ 本体のクラスを作る

class CLogFileEx

    : virtual public CLogFile

    , virtual public CLogFormat

{

public:

  virtual DWORD write( LPCVOID pvData, DWORD dwLen )

  {

    CLogMailslot::WriteFile( pvData, dwLen );

  };

};

こんな感じで、CLogTCPも CLogUDPも CLogMailslotも CLogDBも CLogSerialも

ILog::writeをオーバーライドするだけで作成できるし

仕様追加でフォーマットが変わっても 基本クラスを変えるだけで対応できる

ここで注目なのは、CLogFile定義時には ILog::writeの純仮想関数を宣言していない

もちろんこの時点でnewすれば、エラー

そして、CWriteConverter::writeAでも 実体のない

CWriteConverter::write の純仮想関数を呼んでいる

そして CLogFileEx::write でやっと実体が作られるわけだが

名前と引数が同じなので両方の関数を継承してくれ 見事にnewできるようになる

このソースで注目すべきは 仮想派生クラス。 class キーワードに続く virtual。

仮想派生を行うとオブジェクトの実体は1つしか作られないので

別々のクラスで純仮想関数をオーバーライドしてもちゃんと認識してくれる

しかも親子ではなく兄弟や従兄弟のオーバーライドもOK

この例でいえば CWriteConverterの基本クラスに ILogを仮想継承しても

親をたどっても createもcloseもないのに エラーにならない

ただし、純仮想関数のメソッドを呼ぶ時は例外だ

CWriteConverter::writeAで 実体のないwriteを呼び出ししてるが

writeを兄弟、従兄弟であるCLogFileに定義しても、実体がないと怒られる

不思議だけど、今日の調査結果では 定義は従兄弟でもOK、呼び出すには直系

と言う結果になった

あとは気をつける点は CWriteConverter::writeA の中で

((IWriteConverter*)this)->write(....

と呼び出しているが、やってる事は単純で

writeはCWriteConvereterでは宣言してないので

IWriteConverterのwriteを呼ぶように指示しているのだが

IWriteConverter::write(... とはかけない。

きっと IWriteConverter::writeが仮想関数なのでコンパイル時にアドレスが取れないから

実行時にthisポインタで計算しないと駄目なんだろう

そして今の問題点は、あいまいさの解決。これにはネームスペースが便利といううわさ

そして、キャストの問題

例えば ILog *p; に対して (CLogFileEx*)p とすると

仮想基本クラスから派生クラスへの変更は認められてませんと言われる

あいまいさの解決をすればキャストできないかな・・・

その他、知ってるようでわからない 多重継承

今後も調査したいなぁ・・・