(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
|
演算子と関数オブジェクト ( #10 ) |
関数オブジェクトが役立つのは、アルゴリズムを「クラスの配列」に使用する場合です。既存のクラスを使用する場合、アルゴリズムが使用する演算子を持っていないことがほとんどです。例えば次のようなクラス。
class CNoOperator { public: int m_i; }; このクラスは int 型のメンバ変数を持っていますが、この変数に = などで直接アクセスすることはできません。ピリオド( . )を使ってメンバとしてアクセスする必要があります。なので、このクラスの配列は == 演算子を使用する std::count() には使用できません。 こういう場合には関数オブジェクトが有効です。たとえば、次のようなプレディケートを作成することでこの CNoOperator クラスを std::count_if() に使用できるようになります。 //////////////////////////////////////////////////////////////// // std::count_if() の使用例。 #include <stdio.h> #include <functional> #include <algorithm> // 使用したいクラス。 class CNoOperator { public: int m_i; }; // そのためのプレディケート。 class CPredForNoOperator { public: bool operator()( CNoOperator &p_rcNoOperator ) { if( p_rcNoOperator.m_i == 100 ) return true; return false; } }; // 使用例。 void Use_count_if2() { CNoOperator cAry[6]; int iNo; iNo = std::count_if( cAry, cAry + 6, CPredForNoOperator() ); printf( "%d\n", iNo ); } // 結果 2 ///////////////////////////// このプレディケート CPredForNoOperator は CNoOperator を受け取ることができます。これを std::count_if() に渡すことで、 CNoOperator の配列もアルゴリズムで処理できるようになります。 実際には、この例では == 演算子をグローバル関数としてオーバーロードすることでも解決できます。でも、演算子のオーバーロード関数でも解決できない場合があります。 たとえば、文字列などの「組込型」どうしの場合には、オーバーロード関数を作ることができません。また = 演算子は必ずメンバ関数として作る必要があるため、これも無理です。 そういう時にも関数オブジェクトが役立ちます。 //////////////////////////////////////////////////////////////// // 関数オブジェクトの使用例。 #include <stdio.h> #include <functional> #include <algorithm> // 関数オブジェクト。 class CIsLmn { public: bool operator()( const char *const p_pch ) { const char *const chLmn = "LMN"; return std::equal( chLmn, chLmn + 3, p_pch ); } }; // 使用例。 void Use_CIsLmn() { const char ch1[] = "ABC"; const char ch2[] = "LMN"; const char ch3[] = "XYZ"; const char *chAry[3] = { ch1, ch2, ch3 }; const char **pchLmn = std::find_if( chAry, chAry + 3, CIsLmn() ); if( pchLmn != chAry + 3 ) printf( "%s\n", *pchLmn ); } // 結果 LMN ///////////////////////////// std::find_if() と文字列を受け取る関数オブジェクトを使うことで、文字列どうしの比較を比較を行います。これが std::find() だと、文字列ポインタどうしの比較になってしまうので比較できません。 アルゴリズムだけだと、使える型が限られてきます。これに関数オブジェクトを加えることで、自由度が飛躍的に向上するというわけです。 |
関数オブジェクトの正しい作り方 ( #11 ) |
#08 で言ったように、関数オブジェクトの作り方には「簡単」なものと「複雑」なものとがあります。今回はその「複雑」な方に目を向けてみましょう。
#09 で紹介した CIsCarryUp を例にとって見てみます。まず、関数オブジェクトは std::unary_function か std::binary_function のどちらかから継承する必要があります。このふたつはクラステンプレートになっていて、テンプレート引数に ( ) 演算子のオーバーロード関数の引数と戻り値を指定しなければなりません。 この規則に則ると、次のようなクラスになります。 //////////////////////////////////////////////////////////////// class CIsCarryUpTrue : public std::unary_function< int, bool > { public: bool operator()( int p_i ) { if( 5 <= p_i ) return true; return false; } }; ///////////////////////////// CIsCarryUpTrue は引数をひとつだけ受け取るので std::unary_function から継承します。 std::plus のような引数をふたつ受け取るものは std::binary_function から継承します(ってことは CGenerator のように引数を受け取らないものは継承しなくていいということですね)。 |
|
さて、継承することで変わったことはなんでしょう。実は、基本的な部分はまったく変わっていません。これまでと同じように使えます。唯一変わった部分は「型を明示的に取得できるようになった」点です。
std::unary_function には std::unary_function::argument_type という「型」がメンバとして存在します。この型はテンプレート引数の第1引数と同じ型です。上の例だと int 型ということですね。同じく戻り値の型は std::unary_function::result_type で取得できます。これは bool ですね。 つまり、この型メンバを使うと ( ) 演算子のオーバーロード関数の、引数と戻り値の型を取得できるのです。 この機能は、次のようなクラスを作る時に役立ちます。 //////////////////////////////////////////////////////////////// template< class type_unary_function > class CNewPred : public type_unary_function { typename type_unary_function::argument_type m_Arg; }; ///////////////////////////// CNewPred クラスはテンプレート引数として受け取った型から継承します。そして、テンプレート引数が持つメンバ argument_type 型のメンバ変数を持ちます。 これができるのは、テンプレート引数に std::unary_function 派生クラスを渡したときだけです。先ほどの CIsCarryUpTrue のように、ちゃんとテンプレート引数を指定して継承することで、 CIsCarryUpTrue が持つ情報を間接的に伝えることができるのです。このシステムを使えば、 CNewPred は std::unary_function に対応しさえすれば、多くの関数オブジェクトから簡単に派生できるようになるのです。 この機能は、特に引数をふたつ受け取る関数オブジェクトにとって非常に大事です。次回紹介する「バインダー」は、この機能を使って必要な型を取得します。これから作る関数オブジェクトは、必ずこのような方法で作るようにしましょう。 |
|
さて、 CIsCarryUpTrue に話を戻しましょう。この関数オブジェクトは「5以上かどうか」を調べるプレディケートです。これだけだと本当に使えないので「比較する値を決められる」「クラステンプレート化して比較する型を決められる」ようにしましょう。それが次のクラスです。
//////////////////////////////////////////////////////////////// template< class type_value > class CIsCarryUpPerfect : public std::unary_function< type_value, bool > { type_value m_value; public: CIsCarryUpPerfect( type_value p_value ) { m_value = p_value; } bool operator()( type_value p_i ) { if( m_value <= p_i ) return true; return false; } }; ///////////////////////////// 正直に言えばパーフェクトとはほど遠い( const 関係とか)ですが、これくらいまですればかなり使えるようになるでしょう。「ないものは作る」から「使い倒せるものを作る」へとステップアップすることが STL を無駄なく使う近道と言えるでしょう。 と言っても「あるものは作らない」のがもちろん最善の策です。次回はその方法について見ていきましょう。 |
バインダーで楽しよう ( #12 ) |
関数オブジェクトが特定のアルゴリズムに使用できるかは、引数をいくつ取るかによって変わってきます。たとえば #07 では std::transform() に std::negate を渡していました。これは std::negate が引数をひとつだけ受け取る関数オブジェクトだからです。 std::plus のように引数をふたつ受け取る関数オブジェクトは渡せません。
でも実際は、 #07 の最後でちょっと触れたように、渡す方法が存在します。それは「バインダー」を使うことです。「バインダー」を使うと、関数オブジェクトが持つふたつの引数をひとつにすることができます。なくなった引数の部分には、特定の値を埋めることができます。 では実際に試してみましょう。 //////////////////////////////////////////////////////////////// // std::bind1st() の使用例。 #include <stdio.h> #include <functional> #include <algorithm> void Use_bind1st() { int iSourceAry[] = { 2, 5, 7 }; int iAry[3]; std::transform ( iSourceAry , iSourceAry + 3 , iAry , std::bind1st( std::plus<int>(), 10 ) ); // これ。 for( int *pi = iAry; pi != iAry + 3; ++pi ) printf( "%d, ", *pi ); } // 結果 12, 15, 17, ///////////////////////////// STL のバインダー用関数のひとつが std::bind1st() です。この関数は「引数をひとつ受け取る関数オブジェクト」の代わりに使用できます。 std::bind1st() の第1引数は「引数を2つ受け取る関数オブジェクト」です。この関数オブジェクトの引数をひとつにするのが std::bind1st() の役目です。 std::bind1st() の第2引数は、第1引数の関数オブジェクトの第1引数に渡す値です。関数オブジェクトの引数を2つからひとつに減らすときに、減らした所に埋めるのがこの値です。 つまり std::bind1st() を通すことで std::bind1st( std::plus<int>(), 10 ) が、 std::plus( 10, x ) になるということです。 想像通り、 std::bind2nd() なんてものもあります。これは std::bind2nd( std::plus<int>(), 10 ) が std::plus( x, 10 ) と、2番目の引数に渡してくれます。 std::plus() だと違いが表れませんが、 std::minus() とか比較用プレディケートでは大きな違いです。使い方が同じな分だけ使い間違えやすいと思うので注意してください。 さてここでは std::plus() という初めから用意された関数オブジェクトを使用しました。もちろんこの代わりに自分で作った関数オブジェクトを使用することもできます。 std::bind1st() に渡せる関数オブジェクトは、次の条件を満たしていなければなりません。 1:引数をふたつ受け取る関数オブジェクトである。 2:std::binary_function から継承している。 3:operator()() が const メンバ関数である。 1は当然ですね。2が実は重要です。 std::bind1st() は前回説明した方法で std::binary_function から型の情報を取得するので、継承していないとコンパイルエラーになります。 [注: Visual C++ 6.0 ではコンパイルエラーどころかコンパイラが異常終了しました。ご注意ください] 3は std::bind1st() が受け付ける関数オブジェクト(つまり第1引数)の型が const 参照なので、 const メンバ関数じゃないと呼べないからです。 以上を満たすのは、たとえば次のような関数オブジェクトです。 //////////////////////////////////////////////////////////////// class CPlus : public std::binary_function< int, int, int > { public: int operator()( int p_Rh, int p_Lh ) const { return p_Rh + p_Lh; } }; ///////////////////////////// このような関数オブジェクトなら std::bind1st() に渡すことができます。 と言うよりも、これが関数オブジェクトの一番正しい作り方なのです。自分が作った関数オブジェクトを使い倒すためにも、このようなルール通りの作り方を心がけるようにしましょう。 |
|
|
関数を関数オブジェクトに ( #13 ) |
実は、普通の関数やメンバ関数も、関数オブジェクトにすることができます。前回のバインダーのように、特殊な変換用関数が用意されているのです。
普通の関数を関数オブジェクトとして使う場合には、 std::ptr_fun() を使います。 //////////////////////////////////////////////////////////////// // std::ptr_fun() の使用例。 #include <stdio.h> #include <functional> #include <algorithm> bool Output( int p_i ) { printf( "%d, ", p_i ); return true; } void Use_ptr_fun() { int iSourceAry[] = { 2, 5, 7 }; std::for_each ( iSourceAry , iSourceAry + 3 , std::ptr_fun( Output ) ); } // 結果 2, 5, 7, ///////////////////////////// Output() は整数値を受け取って出力する関数です。この関数へのポインタをそのまま std::ptr_fun() に渡して、それを関数オブジェクトとして渡します。そうすると、 Output() が関数オブジェクトとして呼び出されるのです。 std::ptr_fun() に渡せる関数にはいくつか制約があります。ひとつは引数の数がアルゴリズムから渡される数と一致すること。これは std::bind1st() などを使うことで調整することができます。 もうひとつは、必ず戻り値を返すこと。これは std::ptr_fun() が戻り値を返す仕様になっているため、 void にできないからです。 でも、2点さえ満たしていれば、これまで皆さんが作った関数を関数オブジェクトにすることができて、アルゴリズムに使用することができるのです。 このように使用できるのは、関数だけじゃありません。クラスのメンバ関数も使用できます。 //////////////////////////////////////////////////////////////// // std::mem_fun1() の使用例。 #include <stdio.h> #include <functional> #include <algorithm> class COutput { public: bool Output( int p_i ) { printf( "%d, ", p_i ); return true; } }; void Use_mem_fun1() { COutput cOutput; // 変数を作っておきます。 int iSourceAry[] = { 2, 5, 7 }; std::for_each ( iSourceAry , iSourceAry + 3 , std::bind1st ( std::mem_fun1( &COutput::Output ) // 第1引数 , &cOutput ) ); // 第2引数。 } // 結果 2, 5, 7, ///////////////////////////// ちょっとややこしいですね。順に見ていきましょう。 まずメンバ関数を呼び出したいクラスの変数を作っておきます。まずこれが大事です。 そのあとは先ほどの例とほとんど同じ。 そして最後、 std::for_each() の最後の引数。まず std::bind1st() を使います。メンバ関数ポインタはそのメンバ関数を持つクラス型変数とペアで使います。このふたつを一組にするために std::bind1st() が必要なのです。 std::bind1st() の第1引数には、 std::mem_fun1 を介して、呼び出したいメンバ関数へのポインタを渡します。 std::bind1st() の第2引数にはそのメンバ関数を持つクラス型変数へのポインタを渡します。このクラスを通して、第1引数のメンバ関数が呼ばれるわけです。 こうすることで、特定のクラスの特定のメンバ関数を関数オブジェクトとして使用できるのです。 メンバ関数を関数オブジェクトに使用できるようになると、アルゴリズムの活用の幅が大きく広まるはず。ちょっと難しいかもしれませんが、挑戦してみてください。 |
|
|
(C)KAB-studio 2000 ALL RIGHTS RESERVED. |