コールバックの真価
前回、「関数のポインタ」を使って「コールバック」について考えてきましたが、「コールバック」は何も「関数のポインタ」でなければ実現できないわけではありません
今回は他の方法も見てみましょう
そして「コールバック」の真価へと迫りましょう
関数オブジェクト
さて、まずは「関数のポインタ」の代わりに「関数オブジェクト」を使う方法を見てみましょう
「関数オブジェクト」とは何でしょうか
これは言い換えると「関数呼び出し演算子を持つ任意の型のインスタンス」となるでしょうか
関数呼び出し演算子とは operator() のことです
では「関数オブジェクト」を使った EnumChildObjects を考えてみましょう
前回の例の3つの関数(PrintObjectNames, CountObjects, QueryObjectByName)の実装にはそれぞれ EnumChildObjects に対して異なる関数のポインタを渡していたという点に着目してください
つまり関数ポインタ版の EnumChildObjects には任意の関数のポインタを渡せるようになっていたのでした
では関数オブジェクト版の EnumChildObjects ではどうすればいいでしょうか
それはもうお分かりですね
関数オブジェクト版の EnumChildObjects には任意の関数オブジェクトを渡せるようにすればいいのです
ここで「任意の関数オブジェクト」とは、つまり「関数呼び出し演算子を持つ任意の型のインスタンス」という意味ですね
「任意の型」というところに着目してください
型を指定するにはテンプレートを使うんでしたね
さあ、では実装を考えてみましょう
Fig.1
template<class T> void EnumChildObjects( ISampleObject* pObject, T callback ){ for( int n = 0; n < pObject->GetNumberOfChildren(); ++n ){ ISampleObject* pChildObject = pObject->GetChildObject( n ); if( !callback( pChildObject )){ break; } } }
ではこの新しい関数オブジェクト版の EnumChildObjects を使って前回の例の3つの関数を実装してみましょう
例えば次のようになるでしょうか
Fig.2
struct printobjects_each { bool operator()( ISampleObject* pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); printf( "%s\n", szName ); EnumChildObjects( pObject, *this ); return true; } }; void PrintObjectNames( ISampleObject* pObject ){ EnumChildObjects( pObject, printobjects_each()); } class countobjects_each { int* m_pcObjects; public: bool operator()( ISampleObject* pObject ){ ++(*m_pcObjects); EnumChildObjects( pObject, *this ); return true; } countobjects_each( int* pcObjects ): m_pcObjects(pcObjects) { } }; int CountObjects( ISampleObject* pObject ){ int cObjects = 0; EnumChildObjects( pObject, countobjects_each( &cObjects )); return cObjects; } class queryobjectbyname_each { const char* m_pszName; ISampleObject** m_ppObject; public: bool operator()( ISampleObject* pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( m_pszName, szName )){ *m_ppObject = pObject; } else{ EnumChildObjects( pObject, *this ); } return !!*m_ppObject; } queryobjectbyname_each( const char* pszName, ISampleObject** ppObject ): m_pszName(pszName), m_ppObject(ppObject) { } }; ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ ISampleObject* p = NULL; EnumChildObjects( pObject, queryobjectbyname_each( pszName, &p )); return p; }
あるいは EnumChildObjects に渡す関数オブジェクトを参照渡しにして次のようでもいいかもしれません
Fig.3
template<class T> void EnumChildObjects( ISampleObject* pObject, T& callback ){ for( int n = 0; n < pObject->GetNumberOfChildren(); ++n ){ ISampleObject* pChildObject = pObject->GetChildObject( n ); if( !callback( pChildObject )){ break; } } }
class countobjects_each { int m_cObjects; public: bool operator()( ISampleObject* pObject ){ ++m_cObjects; EnumChildObjects( pObject, *this ); return true; } int result(){ return m_cObjects; } countobjects_each(): m_cObjects(0) { } }; int CountObjects( ISampleObject* pObject ){ countobjects_each callback; EnumChildObjects( pObject, callback ); return callback.result(); } class queryobjectbyname_each { const char* m_pszName; ISampleObject* m_pObject; public: bool operator()( ISampleObject* pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( m_pszName, szName )){ m_pObject = pObject; } else{ EnumChildObjects( pObject, *this ); } return !!m_pObject; } ISampleObject* result(){ return m_pObject; } queryobjectbyname_each( const char* pszName ): m_pszName(pszName), m_pObject(NULL) { } }; ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ queryobjectbyname_each callback( pszName ); EnumChildObjects( pObject, callback ); return callback.result(); }
インターフェイス
さて、前回の「関数のポインタ」を使う方法と今回の「関数オブジェクト」を使う方法と二つの方法を紹介しましたが「コールバック」に使える方法はまだこれだけではありません
「インターフェイス」を使う方法もあります
ここで言う「インターフェイス」とは「抽象基底クラス(ABC)」のことです
覚えていますか?
さあ、では「インターフェイス」を使った新しい EnumChildObjects の実装を考えてみましょう
Fig.4
struct IEnumObjectsCallback { virtual int OnEnum( ISampleObject* pObject ) = 0; }; void EnumChildObjects( ISampleObject* pObject, IEnumObjectsCallback* pCallback ){ for( int n = 0; n < pObject->GetNumberOfChildren(); ++n ){ ISampleObject* pChildObject = pObject->GetChildObject( n ); if( !pCallback->OnEnum( pChildObject )){ break; } } }
ではまたこの新しいインターフェイス版の EnumChildObjects を使って前回の例の3つの関数を実装してみましょう
それは例えば次のようになるでしょうか
Fig.5
struct CEnumObjectsCallback_PrintObjectNames : public IEnumObjectsCallback { int OnEnum( ISampleObject* pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); printf( "%s\n", szName ); EnumChildObjects( pObject, this ); return TRUE; } }; void PrintObjectNames( ISampleObject* pObject ){ CEnumObjectsCallback_PrintObjectNames callback; EnumChildObjects( pObject, &callback ); } class CEnumObjectsCallback_CountObjects : public IEnumObjectsCallback { int* m_pcObjects; public: int OnEnum( ISampleObject* pObject ){ ++(*m_pcObjects); EnumChildObjects( pObject, this ); return TRUE; } CEnumObjectsCallback_CountObjects( int* pcObjects ): m_pcObjects(pcObjects) { } }; int CountObjects( ISampleObject* pObject ){ int cObjects = 0; CEnumObjectsCallback_CountObjects callback( &cObjects ); EnumChildObjects( pObject, &callback ); return cObjects; } class CEnumObjectsCallback_QueryObjectByName : public IEnumObjectsCallback { const char* m_pszName; ISampleObject** m_ppObject; public: int OnEnum( ISampleObject* pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( m_pszName, szName )){ *m_ppObject = pObject; } else{ EnumChildObjects( pObject, this ); } return !!*m_ppObject; } CEnumObjectsCallback_QueryObjectByName( const char* pszName, ISampleObject** ppObject ): m_pszName(pszName), m_ppObject(ppObject) { } }; ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ ISampleObject* p = NULL; CEnumObjectsCallback_QueryObjectByName callback( pszName, &p ); EnumChildObjects( pObject, &callback ); return p; }
あるいはまた次のようにしてもいいかもしれません
Fig.6
class CEnumObjectsCallback_CountObjects : public IEnumObjectsCallback { int m_cObjects; public: int OnEnum( ISampleObject* pObject ){ ++m_cObjects; EnumChildObjects( pObject, this ); return TRUE; } int result(){ return m_cObjects; } CEnumObjectsCallback_CountObjects(): m_cObjects(0) { } }; int CountObjects( ISampleObject* pObject ){ CEnumObjectsCallback_CountObjects callback; EnumChildObjects( pObject, &callback ); return callback.result(); } class CEnumObjectsCallback_QueryObjectByName : public IEnumObjectsCallback { const char* m_pszName; ISampleObject* m_pObject; public: int OnEnum( ISampleObject* pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( m_pszName, szName )){ m_pObject = pObject; } else{ EnumChildObjects( pObject, this ); } return !!m_pObject; } CEnumObjectsCallback_QueryObjectByName( const char* pszName ): m_pszName(pszName), m_pObject(NULL) { } }; ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ CEnumObjectsCallback_QueryObjectByName callback( pszName ); EnumChildObjects( pObject, &callback ); return callback.result(); }
コールバックの本質
さて、ここまで「コールバック」に使える方法として「関数のポインタ」「関数オブジェクト」「インターフェイス」と3つの方法を紹介しました
もうお分かりと思いますが「コールバック」というものは1つのモデルであって、「関数のポインタ」も「関数オブジェクト」も「インターフェイス」も、それはただの実装のテクニックなのです
では「コールバック」の本質とは何でしょうか
それを考えるためにまた前回の例の3つの関数に戻りましょう
この3つの関数はそれぞれ内部で EnumChildObjects という関数を呼び出しています
そしてそこにそれぞれが自分の都合に合った「コールバック関数/オブジェクト/インターフェイス」を渡していますね
これは何を意味しているのでしょうか
EnumChildObjects という関数自体は実に単純な機能の関数です
それは「指定のオブジェクトの子要素を列挙する」というだけの機能しか持ちません
しかしここに「コールバック」という機構があることで、なぜか様々な仕事ができています
これは何を意味しているのでしょうか
ここにこそ「コールバック」の本質があります
「コールバック」の本質とは「柔軟なブラックボックス」あるいは「カスタマイズ可能なブラックボックス」を提供できる点にある。と言えるでしょうか
あるいは逆にその柔軟性によって様々な処理を「ブラックボックス」の中に閉じ込めることができる点にある。と言えるでしょうか
というところで今回はここまでにしましょう。ではまた次回