(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
|
関数オブジェクト ( #06 ) |
前回の最後で「 Alg_printf() なんて std::fill() とほとんど変わらない」と言いました。実際、 std::fill() と Alg_printf() の違いは、代入か printf() か、たった1行の違いです。この違いだけのためにアルゴリズムを作るのは面倒とも言えるでしょう。
STL はそんな面倒なことさせません! その辺のことも、もちろん考えられているのです。こういった問題のために、 STL には「関数オブジェクト」という仕組みが用意されています。これについて見ていきましょう。 関数オブジェクトは「クラステンプレート」の一種です。本当は自分で作るものですが、少しだけ、 STL にも用意されています。まずはこの用意されている関数オブジェクトを、ちょっと「イレギュラー」な方法で使ってみましょう。 //////////////////////////////////////////////////////////////// // std::plus の使用例。 #include <stdio.h> #include <functional> // をインクルードしてね。 void Use_plus() { int i; std::plus<int> cPlus; // これが関数オブジェクト!! i = cPlus( 100, 200 ); printf( "%d\n", i ); } // 結果 300 ///////////////////////////// C++ なスタイルに慣れてない方は「なんじゃこりゃ」と思うかもしれませんね。1行ずつ見ていきましょう。 まず最初の行は int 型変数を作っています。 2行目で、関数オブジェクトの std::plus 型の変数を作ります。この関数オブジェクトはその名の通り「足す」ためのものです。ここでテンプレート引数が int なのは、その足し合わせる値が整数値だからです。もし実数を足し合わせる時には double を指定すればいいわけです。 3行目、いきなり変数 cPlus が関数みたいになってますね。これが「関数オブジェクト」という名前の由来です。オブジェクト(変数)があたかも関数であるかのように振る舞っています。 なぜこのようなことができるのかというと、「 ( ) 」演算子に対して「演算子のオーバーロード」がなされているからです。これまで << とか = とかの例を見てきましたが、こんな不思議なことも「演算子のオーバーロード」を使うことでできるのです。 というわけで、3行目では ( ) のオーバーロード関数、つまり std::plus のメンバ関数のひとつが呼び出されます。そして、約束通り渡されたふたつの整数値が足されて、その結果が戻り値として返されます。この計算を実数で行う場合には、先ほどの「テンプレート引数」を double にすればいいわけですね。 最初から用意されている関数オブジェクトには、他にも std::minus など似たような物がひと通り揃っています。 |
|
さて、関数オブジェクトにどのような意味があるのでしょうか。
関数オブジェクトは「関数ポインタやポリモーフィズムの代わり」と言えるでしょう。「ある関数の呼び出しを変えられるようにする」場合、これまでは関数ポインタやポリモーフィズムを使用していました。関数オブジェクトは、この代わりを「テンプレート」で実現しています。 //////////////////////////////////////////////////////////////// // 関数オブジェクトを受け取る関数。 #include <stdio.h> #include <functional> template< class type_FunctionObject > int FunctionObjectUser( type_FunctionObject p_FunctionObject ) { return p_FunctionObject( 100, 200 ); } // 使用例。 void Use_FunctionObjectUser() { int i; i = FunctionObjectUser( std::plus<int>() ); printf( "%d\n", i ); i = FunctionObjectUser( std::minus<int>() ); printf( "%d\n", i ); } // 結果 300 -100 ///////////////////////////// 関数テンプレート FunctionObjectUser() は、関数オブジェクトを受け取り、ふたつの整数値を渡してその戻り値を返しています。このような関数テンプレートを作れば、「整数値をふたつ受け取り整数値を返す」関数オブジェクトならどれも受け取ることができます。 STL で関数オブジェクトを使用する場合には、 FunctionObjectUser() をアルゴリズムに置き換えて使用します。次はこれについて見ていきましょう。 |
|
|
関数オブジェクトの使い方 ( #07 ) |
上記の関数オブジェクトの使用例は、間違った使い方です。 std::endl を関数として使うようなものですね。ここでは「関数オブジェクトの正しい使い方」について見てみましょう。
まず、 std::negate を紹介しましょう。これは前回の std::plus と同じ、用意されている関数オブジェクトです。 //////////////////////////////////////////////////////////////// // std::negate の使用例。 #include <stdio.h> #include <functional> void Use_negate() { int i; std::negate<int> cNegate; i = cNegate( -100 ); printf( "%d\n", i ); } // 結果 100 ///////////////////////////// 見ての通り符号を反転する関数オブジェクトです。これを、アルゴリズム std::transform() に使用します。 //////////////////////////////////////////////////////////////// // std::transform() の使用例。 #include <stdio.h> #include <functional> #include <algorithm> // をインクルードしてね。 void Use_transform() { int iSourceAry[] = { 2, 5, 7 }; int iAry[3]; std::transform ( iSourceAry , iSourceAry + 3 , iAry , std::negate<int>() ); for( int *pi = iAry; pi != iAry + 3; ++pi ) printf( "%d, ", *pi ); } // 結果 -2, -5, -7, ///////////////////////////// std::transform() は、関数オブジェクトを使用しないと使えないアルゴリズムのひとつです。このアルゴリズムは、第1・2引数に渡された配列を第4引数の関数オブジェクトへと渡して、その結果を第3引数の配列へと格納するという機能を持っています。つまり「配列を関数オブジェクトに掛ける」という機能のみを持ったアルゴリズムということですね。 std::transform() は、だいたい次のようになっています。 template < class type_InputIter , class type_OutputIter , class type_UnaryOperation > type_OutputIter transform ( type_InputIter p_Begin , type_InputIter p_End , type_OutputIter p_Result , type_UnaryOperation p_Fuctional ) { for( ; p_Begin != p_End; ++p_Begin, ++p_Result ) *p_Result = p_Fuctional( *p_Begin ); return p_Result; } std::find() などと比べてみると、 for ループの部分などは同じことが分かると思います。違うのはその中身、 p_Fuctional という関数オブジェクトを使って ( ) 演算子のオーバーロード関数を呼びだしている部分です。前回の例と同じく、関数オブジェクトの型はテンプレート引数になっているので、一応どんな関数オブジェクトでも渡すことができます。でも、制限もあります。 この関数オブジェクトは引数をひとつしか受け取っていませんね。ということは、前回の std::plus は使えないということです。無理に使おうとしてもコンパイルエラーが発生します。こういった制限のおかげで、どんな関数オブジェクトでも渡せる、というわけではないのです。 どのアルゴリズムにどの関数オブジェクトが使えるのか、ということは、アルゴリズムの中身を見たり、渡す配列の数を調べたりすることで判別します。また、この std::transform() に std::plus を渡す方法が、実はちゃんと用意されていたりもします。 |
関数オブジェクトの作り方 ( #08 ) |
STL が用意してくれている関数オブジェクトは非常に少ないので、基本的にはプログラマーが自分で作る必要があります。その方法を見てみましょう。
今回は std::generate() というアルゴリズムを使用します。このアルゴリズムは、 std::fill() の「関数オブジェクト使用版」とでも言えるもので、通常の値ではなく関数オブジェクトの戻り値を配列に格納します。 std::generate() の関数オブジェクトには、引数を渡しません。そこで引数を受け取らない関数オブジェクトを作成します。 関数オブジェクトを作るとき、簡単に作る方法と本格的に作る方法の2種類あります。今回は簡単な方法を紹介しましょう。 //////////////////////////////////////////////////////////////// class CGenerator { public: int operator()() { return 100; } }; ///////////////////////////// このクラス CGenerator は、 () 演算子をオーバーロードしています。基本的に、この演算子をオーバーロードさえしていれば、関数オブジェクトになることができます。 この関数オブジェクトは、引数を受け取らず、 100 を返すだけという非常に単純な関数オブジェクトです。引数を受け取らないため、前回紹介した std::transform() には使用できませんが、代わりに std::generate() には使用できます。 std::generate() は関数オブジェクトを呼ぶときに引数を渡さないからです。 というわけで、早速使ってみましょう。 //////////////////////////////////////////////////////////////// // std::generate() の使用例。 #include <stdio.h> #include <functional> #include <algorithm> void Use_generate() { int iAry[3]; std::generate( iAry, iAry + 3, CGenerator() ); for( int *pi = iAry; pi != iAry + 3; ++pi ) printf( "%d, ", *pi ); } // 結果 100, 100, 100, ///////////////////////////// std::fill() と std::generate() の違いは、第3引数に渡しているのが「値」か「関数オブジェクト」かという違いです。関数オブジェクトが渡され、戻り値が値として格納されます。この例だと、 CGenerator::operator()() の戻り値が配列の全要素に格納されます。 「でもこれって std::fill() 使えばいいんじゃないの?」と思うかもしれません。ですが、関数オブジェクトを使うことで初めてできるようになることがたくさんあります。 例えば、 CGenerator の代わりにこんな関数オブジェクトを使ってみたらどうでしょう。 //////////////////////////////////////////////////////////////// class CForGenerator { int m_i; public: CForGenerator() { m_i = 0; } int operator()() { return m_i++; } }; ///////////////////////////// この関数オブジェクトを先ほどの例を使って std::generate() に渡すと 0, 1, 2, という結果が返ってきます。これは関数オブジェクトが「クラスの変数」だというメリットを使用した例です。「ただの値」が「関数オブジェクト」になることで、自由度が飛躍的に増します。にもかかわらず、使い方はどちらも同じ、と使いやすさなども保たれているというわけです。 |
プレディケート ( #09 ) |
関数オブジェクトの中には「プレディケート( predicate )」と呼ばれるものがあります。日本語では「述語関数」とか「述語オブジェクト」とか呼ばれています。プレディケートを使うアルゴリズムを「述語バージョン」とか呼んだりもします。
プレディケートは関数オブジェクトの中でも「 ( ) 演算子のオーバーロード関数の引数がひとつ以上あり、戻り値が bool のもの」を指します。プレディケートは引数を受け取り、それが「正しいか間違っているか」などを適当に判別して、結果を bool 型として返します。ということは前回紹介した CGenerator などはプレディケートじゃないということですね。 プレディケートもほとんど用意されていないので、自分で作る必要があります。プレディケートの例を見てみましょう。 //////////////////////////////////////////////////////////////// class CIsCarryUp { public: bool operator()( int p_i ) { if( 5 <= p_i ) return true; return false; } }; ///////////////////////////// ( ) 演算子のオーバーロード関数が、引数ひとつを受け取って、戻り値が bool 型ですね。このプレディケートは、引数が 5 以上だと true を返す仕様になっています。 この使用例を見てみましょう。「引数がひとつのプレディケート」を受け取るアルゴリズムのひとつに std::count_if() があります。このアルゴリズムは、配列の各要素をプレディケートに渡して、 true の数をカウントします。 //////////////////////////////////////////////////////////////// // std::count_if() の使用例。 #include <stdio.h> #include <functional> #include <algorithm> void Use_count_if() { int iAry[] = { 3, 5, 1, 6, 3, 1 }; int iNo; iNo = std::count_if( iAry, iAry + 6, CIsCarryUp() ); printf( "%d\n", iNo ); } // 結果 2 ///////////////////////////// std::count_if() は各要素を CIsCarryUp::operator()() に渡して true が返ってきた回数を返します。 これを「関数オブジェクトを使用しない」 std::count() と比べてみましょう。 //////////////////////////////////////////////////////////////// // std::count() の使用例。 #include <stdio.h> #include <algorithm> void Use_count() { int iAry[] = { 3, 5, 1, 6, 3, 1 }; int iNo; iNo = std::count( iAry, iAry + 6, 1 ); printf( "%d\n", iNo ); } // 結果 2 ///////////////////////////// std::fill() と std::generate() のときもそうだったように、たいがいのアルゴリズムは「値」と「関数オブジェクト」との両方を持っています。 std::count() はプレディケートを使用せず、各要素と第3引数を == 演算子で直接比較します。 std::count_if() は、 == 演算子をプレディケートに置き換えたものと考えればいいでしょう。プレディケートという「メンバ関数」に置き換えることで、単なる == 演算子にはできない、さまざまな比較ができるようになるのです。 |
(C)KAB-studio 2000, 2004 ALL RIGHTS RESERVED. |