コールバックの薦め
「コールバック関数」ってよく聞きますよね
今回はその「コールバック」の使い方を覚えましょう
その前に
その前に、関数のポインタがなんだかわからないという人や、関数のポインタの宣言の仕方がわからないという人は C/C++ 変数宣言の読み方入門 や C/C++ ポインタ入門 を先にご覧になった方がいいかもしれません
具体的には
まず具体的な例を挙げますが、「コールバック関数」というのは文法的には関数のポインタを引数に取る関数に指定するその関数のことです
この例では foo 関数はパラメータに指定された callback という関数のポインタを介して bar 関数を呼び出すことができるようになっていますね
この仕組みが「コールバック」です
Fig.1
void foo( size_t celt, int* pelt, void (*callback)(int)){ for( size_t n = 0; n < celt; ++n ){ callback( pelt[n] ); } } void bar( int e ){ debug_printf( "%d\n", e ); } void baz( size_t celt, int* pelt ){ . . . foo( celt, pelt, bar ); . . . }
さて、そもそも「コールバック」とはなんでしょう
コールバックとは
「コールバック」というのは英語の「callback」そのままですが、これは直訳すると「呼び戻す」とか「折り返し電話する」とかいうような意味のようです
プログラミングの文脈で言う「コール」というのはもちろん電話のことではありません
「コール」とは「(ある処理を)呼び出す行為」のことですよね
「コールバック」というのは言葉の意味としては「(呼び出された側から呼び出した側を)呼び出し返す行為」ということになります
呼び出し返す
ここで「呼び出し返す」という言葉に注目してください
「呼び出し返す」とはつまり「(関数を)呼び出し返す」ということです
この「呼び出し返される関数」のことを「コールバック関数」と言います
上記の具体例で言うところの bar 関数のことです
2つの主体
またもう一つここで注目して欲しいのは「呼び出した側」と「呼び出された側」という2つの主体が存在していることです
上記の具体例で言うと「呼び出した側」は baz 関数で、「呼び出された側」は foo 関数です
ただ単純に「ある処理からまたある処理を呼び出す行為」というだけでは「コールバック」とは言いません
それは手順が二段階になっただけで、そこには一つの主体しかありませんね
「コールバック」と言うためには、そこに意味上の分断がある必要があります
これが重要です
即ち、その境界を越えて「呼び出された側」から「呼び出した側」へと「コール」し返す行為を「コールバック」と言うのです
ですから「コールバック」というのは「ある意味上の境界を越えてコールバック関数を呼び出す行為」であるとも言えます
上記の具体例で言うなら、foo 関数という閉じた文脈からその外側にある baz 関数の文脈に属する bar 関数を呼び出す行為がコールバックです
境界を越えて
そして意味上の境界を越えて処理が戻ってくるわけですから呼び出し返される「コールバック関数」というのは「呼び出した側」の主体に属するものであるということもわかります
「呼び出した側」の主体に属するのですから「コールバック関数」は「呼び出す側」が指定すべきものであるということもわかりますね
上記の具体例で言うと bar 関数を指定しているのは baz 関数の中ですね
具体例
さて、言葉の意味としての「コールバック」を押さえたところでまた別の具体的なコードを使って「コールバック」を考えてみることにしましょう
今ここで次のようなオブジェクトがあるとします
Fig.2
struct ISampleObject { virtual int GetNumberOfChildren() = 0; virtual ISampleObject* GetChildObject( int n ) = 0; virtual int GetName( int cchBuffer, char* pszBuffer ) = 0; };
見ての通りこのオブジェクトは再帰的な構造(ツリー構造)を表していますね
このオブジェクトに対して次のような3つの関数を書くとするとその実装はどのようになるでしょうか
それぞれについて書いてみましょう
Fig.3
// 指定されたオブジェクト以下の階層にある全てのオブジェクトの名前を出力する。 void PrintObjectNames( ISampleObject* pObject ); // 指定されたオブジェクト以下の階層にあるオブジェクトを数える。 int CountObjects( ISampleObject* pObject ); // 指定されたオブジェクト(以下の階層)から指定された名前を持つオブジェクトを探す。 ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName );
オブジェクトが再帰的なのですから、コードも再帰的にしておくのが素直でしょう
Fig.4
void PrintObjectNames( ISampleObject* pObject ){ int cChildren = pObject->GetNumberOfChildren(); for( int n = 0; n < cChildren; ++n ){ ISampleObject* pChildObject = pObject->GetChildObject( n ); char szName[MAX_SAMPLE_OBJECT_NAME]; pChildObject->GetName( elementsof( szName ), szName ); printf( "%s\n", szName ); PrintObjectNames( pChildObject ); } } int CountObjects( ISampleObject* pObject ){ int cChildren = pObject->GetNumberOfChildren(); int cObjects = cChildren; for( int n = 0; n < cChildren; ++n ){ ISampleObject* pChildObject = pObject->GetChildObject( n ); cObjects += CountObjects( pChildObject ); } return cObjects; } ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ ISampleObject* p = NULL; int cChildren = pObject->GetNumberOfChildren(); for( int n = 0; n < cChildren; ++n ){ ISampleObject* pChildObject = pObject->GetChildObject( n ); char szName[MAX_SAMPLE_OBJECT_NAME]; pChildObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( pszName, szName )){ p = pChildObject; } else{ p = QueryObjectByName( pChildObject, pszName ); } if( p ){ break; } } return p; }
さて、ここでFig.4を見ると3つとも同じようなループがあることに気付きますね
こう云うときにはコードを纏められるという合図です
ではこの3つの関数に共通する部分を関数として括り出してみましょう
そのためにまずこの3つの関数が共通してやっていることを言葉にしてみます
そうするとそれは「指定されたオブジェクトの子要素を列挙し、そのそれぞれに対して所定の処理をする」ということになりますよね
そこから今書くべき関数は、指定のオブジェクトの子要素を列挙してそのそれぞれについて何らかの関数を実行すればよさそうだということがわかりますね
「何らかの関数を実行する」ということはつまり実行すべき関数は決まっていないわけですから、状況によってその実行すべき関数を指定できるようにすべきだということもわかりますね
つまりそこは引数になります。関数を指定する引数です
そうしたことを踏まえて3つの関数に共通する部分を関数として実装してみると次のようになるでしょうか
Fig.5
void EnumChildObjects( ISampleObject* pObject, void (*pCallback)( ISampleObject* )){ // 指定されたオブジェクトの子要素を列挙する int cChildren = pObject->GetNumberOfChildren(); for( int n = 0; n < cChildren; ++n ){ // 子要素のそれぞれに対して指定された処理をする ISampleObject* pChildObject = pObject->GetChildObject( n ); pCallback( pChildObject ); } }
ではこの EnumChildObjects という関数を使って先の3つの関数を実装するとどのようになるか実際に書いてみましょう
Fig.6
static void EnumChildObjectsCallback_PrintObjectNames( ISampleObject* pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); printf( "%s\n", szName ); } void PrintObjectNames( ISampleObject* pObject ){ EnumChildObjects( pObject, EnumChildObjectsCallback_PrintObjectNames ); } static int g_cObjects; static void EnumChildObjectsCallback_CountObjects( ISampleObject* pObject ){ ++g_cObjects; EnumChildObjects( pObject, EnumChildObjectsCallback_CountObjects ); } int CountObjects( ISampleObject* pObject ){ g_cObjects = 0; EnumChildObjects( pObject, EnumChildObjectsCallback_CountObjects ); return g_cObjects; } struct _QueryObjectByNameParams { const char* pszName; ISampleObject* pObject; }; static struct _QueryObjectByNameParams g_QueryObjectByNameParams; static void EnumChildObjectsCallback_QueryObjectByName( ISampleObject* pObject ){ if( !g_QueryObjectByNameParams.pObject ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( g_QueryObjectByNameParams.pszName, szName )){ g_QueryObjectByNameParams.pObject = pObject; } else{ EnumChildObjects( pObject, EnumChildObjectsCallback_QueryObjectByName ); } } } ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ g_QueryObjectByNameParams.pszName = pszName; g_QueryObjectByNameParams.pObject = NULL; EnumChildObjects( pObject, EnumChildObjectsCallback_QueryObjectByName ); return g_QueryObjectByNameParams.pObject; }
どうでしょう。こんな感じになりましたが、これでは問題になるところが2つありますね
わかりますか?
1つは、この実装ではグローバル変数を使っているところです
このように局所的な問題の解決にグローバル変数を使うのは美しくありませんし、またマルチスレッド耐性にも問題を生じます
もう1つは、QueryObjectByName で目的のオブジェクトを見つけた後も検索を無駄に続けてしまっているところです
この2つの問題はどうすれば解決できるでしょうか
まずグローバル変数を使っている問題については、呼び出される関数にパラメータを渡せるようにすることで解決できます
ただ、パラメータを渡すといっても渡したいパラメータは関数ごとに異なるのでどれといって予め限定してしまうことはできません
ですからこのパラメータは void * にしましょう
そして無駄に検索を続けてしまっている問題については、呼び出される関数の戻り値によって処理を続けるか中断するかを択べるようにすることで解決できます
この2つの問題に対処してFig.5の EnumChildObjects を修正すると次のようになるでしょうか
Fig.7
void EnumChildObjects( ISampleObject* pObject, int (*pCallback)( ISampleObject*, void* ), void* pvContext ){ // 指定されたオブジェクトの子要素を列挙する int cChildren = pObject->GetNumberOfChildren(); for( int n = 0; n < cChildren; ++n ){ // 子要素のそれぞれに対して指定された処理をする。パラメータも渡す ISampleObject* pChildObject = pObject->GetChildObject( n ); int fContinue = pCallback( pChildObject, pvContext ); if( !fContinue ){ // コールバック関数の戻り値が FALSE なら列挙を中断する break; } } }
ではまたこの修正した EnumChildObjects を使って3つの関数の実装も修正してみましょう
Fig.8
static int EnumChildObjectsCallback_PrintObjectNames( ISampleObject* pObject, void* pvContext ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); printf( "%s\n", szName ); EnumChildObjects( pObject, EnumChildObjectsCallback_PrintObjectNames, pvContext ); return TRUE; } void PrintObjectNames( ISampleObject* pObject ){ EnumChildObjects( pObject, EnumChildObjectsCallback_PrintObjectNames, NULL ); } static int EnumChildObjectsCallback_CountObjects( ISampleObject* pObject, void* pvContext ){ int* pcObjects = ( int* )pvContext; ++(*pcObjects); EnumChildObjects( pObject, EnumChildObjectsCallback_CountObjects, pvContext ); return TRUE; } int CountObjects( ISampleObject* pObject ){ int cObjects = 0; EnumChildObjects( pObject, EnumChildObjectsCallback_CountObjects, &cObjects ); return cObjects; } struct _QueryObjectByNameParams { const char* pszName; ISampleObject* pObject; }; static int EnumChildObjectsCallback_QueryObjectByName( ISampleObject* pObject, void* pvContext ){ struct _QueryObjectByNameParams* pParams = ( struct _QueryObjectByNameParams* )pvContext; char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( pParams->pszName, szName )){ pParams->pObject = pObject; } else{ EnumChildObjects( pObject, EnumChildObjectsCallback_QueryObjectByName, pvContext ); } return !!pParams->pObject; } ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ struct _QueryObjectByNameParams params; params.pszName = pszName; params.pObject = NULL; EnumChildObjects( pObject, EnumChildObjectsCallback_QueryObjectByName, ¶ms ); return params.pObject; }
static な関数が外に出てしまってイヤだ。という場合にはこのようにしてもいいかもしれません
Fig.9
void PrintObjectNames( ISampleObject* pObject ){ struct CPrintObjectNames { static int callback( ISampleObject* pObject, void* pvContext ){ char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); printf( "%s\n", szName ); EnumChildObjects( pObject, callback, pvContext ); return TRUE; } }; EnumChildObjects( pObject, CPrintObjectNames::callback, NULL ); } int CountObjects( ISampleObject* pObject ){ struct CCountObjects { static int callback( ISampleObject* pObject, void* pvContext ){ int* pcObjects = ( int* )pvContext; ++(*pcObjects); EnumChildObjects( pObject, callback, pvContext ); return TRUE; } }; int cObjects = 0; EnumChildObjects( pObject, CCountObjects::callback, &cObjects ); return cObjects; } ISampleObject* QueryObjectByName( ISampleObject* pObject, const char* pszName ){ struct CQueryObjectByName { struct params_t { const char* pszName; ISampleObject* pObject; }; static int callback( ISampleObject* pObject, void* pvContext ){ struct params_t* pParams = ( struct params_t* )pvContext; char szName[MAX_SAMPLE_OBJECT_NAME]; pObject->GetName( elementsof( szName ), szName ); if( !lstrcmp( pParams->pszName, szName )){ pParams->pObject = pObject; } else{ EnumChildObjects( pObject, callback, pvContext ); } return !!pParams->pObject; } }; struct CQueryObjectByName::params_t params; params.pszName = pszName; params.pObject = NULL; EnumChildObjects( pObject, CQueryObjectByName::callback, ¶ms ); return params.pObject; }
どうでしょうか
元のコードにあった似たような処理は EnumChildObjects という単純な機能を持った関数へとまとめる事ができました
さらにそこに「コールバック」という機構を持たせる事によってこの3つの関数だけでなく様々な処理に応用することもできるようになりました
どうですか?「コールバック」って便利そうじゃありませんか?
というところで今回はここまでにしましょう。ではまた次回