The ABC's of ABC
今回は「Abstract Base Class(抽象基底クラス)」についてお話しましょう
「抽象基底クラス」とは「pure virtual function(純粋仮想関数)だけからなるクラス」のことです
「純粋仮想関数」というのは実装を持たない仮想関数のことです
ここで言う「実装を持たない」ということは「空の実装さえも持たない、純粋にプロトタイプだけの」という意味です
「純粋仮想関数」は実装を持っていないのでそのままでは呼び出すことはできません
呼び出すことができないどころか1つでも「純粋仮想関数」をメンバに持つクラスは instantiate(インスタンス化)することさえできません
そんなものがなぜ存在しているのでしょうか
それはインターフェイスを実装から分離するためです
インターフェイスを実装から分離するとはどういうことでしょうか
これは言い換えれば「見せる必要の無い実装の詳細は隠せ」ということです
「実装の詳細」というのは「作り手側の都合」です
使い手側にしてみればそんなものはどうでもいいことです
使い手側はやりたいことができればそれでいいのです
それがどのように実現されているかということは使い手にとってはまったく何の価値もないことです
これはプログラミングに限らず多くのことでそうですが、これを C++ でのプログラミングに限って言うなら、あるクラスを使うプログラマにとって重要なのは「そのクラスを使って何ができるか」であって「そのクラスがどう実装されているか」ではない。ということです
当たり前のことですね
この当たり前のことを実現するための機能が「純粋仮想関数」と「抽象基底クラス」です
少し話が抽象的になったので、ここでコンパイラがやっていることを考えてみましょう
通常の仮想関数はデフォルトでは仮想関数テーブルに基底クラスの仮想関数のアドレスが入っています
Fig.1
struct CBaseClass { virtual void aaa(); virtual void bbb(); ... CBaseClass(); virtual ~CBaseClass(); };
struct CBaseClass; struct CBaseClassVtbl { void (*aaa)( struct CBaseClass* const this ); void (*bbb)( struct CBaseClass* const this ); ... void (*_CBaseClass)( struct CBaseClass* const this ); }; struct CBaseClass { struct CBaseClassVtbl* lpVtbl; ... }; void CBaseClass_aaa( struct CBaseClass* const this ); void CBaseClass_bbb( struct CBaseClass* const this ); void CBaseClass__CBaseClass( struct CBaseClass* const this ); struct CBaseClassVtbl c_CBaseClassVtbl = { CBaseClass_aaa, CBaseClass_bbb, ... CBaseClass__CBaseClass, }; CBaseClass_CBaseClass( struct CBaseClass* const this ){ this->lpVtbl = c_CBaseClassVtbl; ... }
派生クラスが仮想関数をオーバーライドすると仮想関数テーブルのその仮想関数の部分は派生クラスの仮想関数のアドレスに置き換わります
Fig.2
struct CDerivedClass : public CBaseClass { virtual void bbb(); virtual void ccc(); ... CDerivedClass(); virtual ~CDerivedClass(); };
struct CDerivedClass; struct CDerivedClassVtbl { void (*aaa)( struct CBaseClass* const this ); void (*bbb)( struct CBaseClass* const this ); void (*ccc)( struct CDerivedClass* const this ); ... void (*_CDerivedClass)( struct CDerivedClass* const this ); }; struct CDerivedClass { struct CDerivedClassVtbl* lpVtbl; ... }; void CDerivedClass_bbb( struct CDerivedClass* const this ); void CDerivedClass_ccc( struct CDerivedClass* const this ); void CDerivedClass__CDerivedClass( struct CDerivedClass* const this ); struct CDerivedClassVtbl c_CDerivedClassVtbl = { CBaseClass_aaa, (void (*)( struct CBaseClass* const ))CDerivedClass_bbb, CDerivedClass_ccc, ... CDerivedClass__CDerivedClass, }; CDerivedClass_CDerivedClass( struct CDerivedClass* const this ){ CBaseClass_CBaseClass(( struct CBaseClass* )this ); this->lpVtbl = c_CDerivedClassVtbl; ... }
ところが「純粋仮想関数」は実装を持たないのでデフォルトで仮想関数テーブルに入れておく基底クラスの仮想関数のアドレスというものがありません。これはイメージとしては仮想関数テーブルに NULL が入っているようなものでしょうか
Fig.3
struct CDerivedClass2 : public CBaseClass { virtual void ddd() = 0; ... CDerivedClass2(); virtual ~CDerivedClass2(); };
struct CDerivedClass2; struct CDerivedClass2Vtbl { void (*aaa)( struct CBaseClass* const this ); void (*bbb)( struct CBaseClass* const this ); void (*ddd)( struct CDerivedClass2* const this ); }; struct CDerivedClass2 { struct CDerivedClass2Vtbl* lpVtbl; ... }; void CDerivedClass2__CDerivedClass2( struct CDerivedClass2* const this ); struct CDerivedClass2Vtbl c_CDerivedClass2Vtbl = { CBaseClass_aaa, (void (*)( struct CBaseClass* const ))CBaseClass_bbb, NULL, ... CDerivedClass2__CDerivedClass2, }; CDerivedClass2_CDerivedClass2 struct CDerivedClass2* const this ){ CBaseClass_CBaseClass(( struct CBaseClass* )this ); this->lpVtbl = c_CDerivedClass2Vtbl; ... }
仮想関数テーブルに NULL が入ったままコンパイルを通してしまうと実行時にエラーになってしまいますから、そんなことのないように「純粋仮想関数」が実装されていない場合にはコンパイル時にエラーとして検出できるようになっています
これが「純粋仮想関数」を実装しないとクラスをインスタンス化できない理由です
ところで「抽象基底クラス」はそのメンバが「純粋仮想関数」だけなので仮想関数テーブルが全て NULL であるとイメージしてもよさそうですが、そういうことではありません
「抽象基底クラス」は名前に「抽象」と付いているように一切実装を持たないのでイメージとしては次のようにただ型宣言があるだけだと考えた方がより適切でしょう
Fig.4
struct CAbstractBaseClass { virtual void aaa() = 0; virtual void bbb() = 0; virtual void ccc() = 0; };
struct CAbstractBaseClass; struct CAbstractBaseClassVtbl { void (*aaa)( struct CAbstractBaseClass* const this ); void (*bbb)( struct CAbstractBaseClass* const this ); void (*ccc)( struct CAbstractBaseClass* const this ); }; struct CAbstractBaseClass { struct CAbstractBaseClassVtbl* lpVtbl; };
そしてこの「抽象基底クラス」は自分自身のインスタンスというものは持つことができず、派生クラスのインスタンスを指すポインタとしてしか使えません
というよりも正にそのために存在しているのです
さてここからは具体的に行きましょう
みなさんは普段クラスを公開するときに Fig.5 のようなクラス定義をヘッダファイルに入れているでしょうか
Fig.5
// この例では FILE* を使って実装してるみたい class CFile { FILE* m_fp; public: bool Open( const char* path, const char* fopen_mode ); void Close(); size_t Read( void* pBuffer, size_t cbBlock, size_t cBlocks ); int Seek( long lOffset, int nFlags ); long Tell(); CFile(); ~CFile(); };
そしてそれを使い手側では例えば Fig.6 のようにしているでしょうか
Fig.6
CFile sample_file; if( sample_file.Open( _T("sample.file"), _T("rb"))){ sample_file.Seek( 0, SEEK_END ); long lFileSize = sample_file.Tell(); . . . sample_file.Close(); }
この場合このクラスの使い手側からアクセス可能なのは public メソッド(メンバ関数)である Open や Read などだけであって private なメンバ変数などには当然アクセスできませんね
使い手側からアクセスできないものは使い手側にとっては必要のないものですよね
使い手側に必要のないものが使い手側に見えている必要はないとは思いませんか?
もちろん private メンバも作り手側にとって必要なのは当たり前です
しかしそんなものは「作り手側の都合」でしかありません
作り手側の都合なんてものは使い手側にとっては全くどうでもいいことです
ソフトウェアを設計する上で重要なのはこの「使い手側にとってどうか」という視点です
何度も言うようですがこの「使い手側にとって」という視点を常に忘れないようにしてください
この場合でも例えば作り手側で「このクラスを使う人には関係ないけれども、内部的に使ってた FILE* をファイルハンドルに変更した」なんていうことになった場合のことを考えてみてください
「このクラスを使う人には関係ないけれども」といいながら現実にはこのクラスを使っている人は全員ビルドし直す羽目になっていますよね
今書いているコードはこんな風になっていませんか?
だから「抽象基底クラス」です
まずは「使い手側にとって必要な」public メソッド(メンバ関数)だけを「抽象基底クラス」に分離しましょう
Fig.7
struct IFile { virtual bool Open( const char* path, const char* fopen_mode ) = 0; virtual void Close() = 0; virtual size_t Read( void* pBuffer, size_t cbBlock, size_t cBlocks ) = 0; virtual long Seek( long lOffset, int nFlags ) = 0; virtual long Tell() = 0; };
そうしたらこの新しい IFile を今までの CFile の基底クラス(Base Class)にしてあげます
Fig.8
class CFile : public IFile { FILE* m_fp; public: virtual bool Open( const char* path, const char* fopen_mode ); virtual void Close(); virtual size_t Read( void* pBuffer, size_t cbBlock, size_t cBlocks ); virtual int Seek( long lOffset, int nFlags ); virtual long Tell(); CFile(); virtual ~CFile(); };
こうしておいて使い手側では「抽象基底クラス」である IFile だけを使うようにします
Fig.9
IFile* pFile = /* IFile のインスタンスが欲しいけどどうすれば・・・ */; if( pFile ){ if( pFile->Open( _T("sample.file"), _T("rb"))){ pFile->Seek( 0, SEEK_END ); long lFileSize = pFile->Tell(); . . . pFile->Close(); } // ここで IFile のインスタンスを破棄したい }
ただ、ここで1つ問題があります
今や使い手側に見えるのは「抽象基底クラス」だけなので使い手側はそれをインスタンス化することができないということです
しかしここで欲しいのは「抽象基底クラス」自身のインスタンスではありません
「抽象基底クラス」は純粋にただのインターフェイスなのでそのインスタンスというものは存在しません
ここで欲しいインスタンスとは「抽象基底クラス」のメソッドを全て実装した何らかの派生クラスのインスタンスですね
ですから、ここでやるべきはその派生クラスをインスタンス化することです
派生クラスをインスタンス化するといっても、ここで次のようにしてしまっては結局 CFile を使い手側に見せることになってしまいますから「抽象基底クラス」を使う意味がありませんね
IFile* pFile = new CFile;
ではどうすればいいでしょう
Fig.10 のように「抽象基底クラス」を実装した派生クラスをインスタンス化する関数を用意することで解決できます
ここでは CFile に CreateInstance という新しい static メソッド(メンバ関数)を追加しましょう
Fig.10
class CFile : public IFile { FILE* m_fp; public: virtual bool Open( const char* path, const char* fopen_mode ); virtual void Close(); virtual size_t Read( void* pBuffer, size_t cbBlock, size_t cBlocks ); virtual int Seek( long lOffset, int nFlags ); virtual long Tell(); static IFile* CreateInstance(); CFile(); virtual ~CFile(); }; IFile* CFile::CreateInstance(){ return new CFile; } IFile* FileCreate(){ return CFile::CreateInstance(); }
こうすることで使い手側は派生クラスの定義が見えなくても派生クラスをインスタンス化できるようになりました
これは「オブジェクトの生成に関する実装の詳細を隠蔽した」ということでもあります
ところで「生成」とくれば対になるものがありますね
そうですね。対となる「オブジェクトの破棄に関する実装の詳細」も隠蔽すべきだと云うことですね
ここでは IFile に Destroy という新しいメソッド(メンバ関数)を追加してみましょう
Fig.11
struct IFile { virtual bool Open( const char* path, const char* fopen_mode ) = 0; virtual void Close() = 0; virtual size_t Read( void* pBuffer, size_t cbBlock, size_t cBlocks ) = 0; virtual long Seek( long lOffset, int nFlags ) = 0; virtual long Tell() = 0; virtual void Destroy() = 0; };
その実装は Fig.12 のようになります
Fig.12
class CFile : public IFile { FILE* m_fp; public: virtual bool Open( const char* path, const char* fopen_mode ); virtual void Close(); virtual size_t Read( void* pBuffer, size_t cbBlock, size_t cBlocks ); virtual int Seek( long lOffset, int nFlags ); virtual long Tell(); virtual void Destroy(); static IFile* CreateInstance(); CFile(); virtual ~CFile(); };
void CFile::Destroy(){ delete this; }
これでできあがりです。 Fig.6 は Fig.13 のようになりますね
Fig.13
IFile* pFile = FileCreate(); if( pFile ){ if( pFile->Open( _T("sample.file"), _T("rb"))){ pFile->Seek( 0, SEEK_END ); long lFileSize = pFile->Tell(); . . . pFile->Close(); } pFile->Destroy(); pFile = NULL; }
こうしておけば今後どんなに実装が変わろうとも元となる「抽象基底クラス」さえ変わらなければ使い手側には一切影響は出なくなりますね
というところで、最後にもう1つだけ
今追加したこの Destroy というメソッドはインスタンスを破棄するためのものなのですが、このようなメソッドは1つのインスタンスを複数人で参照することがあるようなときには問題になります
次回はそのような問題に対応できる「リファレンスカウンタ」というものについて学びましょう
では今回はこの辺で