C++言語解説:STL基礎(4)関数オブジェクト
2003-08-25 |
[C++ Index Top]
[Prev]
|
[概要] コンテナやアルゴリズムを利用する際、コンテナ内のデータ要素への処理、又は 要素をオペランドとしたコンテナ間の処理等を行いたい場合があります。STLでは 四則/論理演算等の処理に対応した関数オブジェクトと呼ばれるクラスが用意され ていますが、ここでは「関数オブジェクトとは何か」について考えます。 [構成]・準備:コンテナ要素に処理を施す * transform()アルゴリズム * 関数ポインタ * 関数テンプレートと実行速度への懸念 ・関数オブジェクト * operator()が定義されたclass * 関数オブジェクトの基底クラス * 組み込み関数オブジェクト ●準備:コンテナ要素に処理を施す ◆transform()アルゴリズム ・transform()アルゴリズムはコンテナ要素に関数を適用し、結果を指定したコンテ ナに格納します。形式は以下の2種になります。 (1) template <class InputIter, class OutputIter, class Func> OutputIter transform(InputIter start, InputIter end, OutputIter result, Func UnaryFunc); (2) template <class InputIter1, classInputIter2, class OutputIter, class Func> OutputIter transform(InputIter1 start1, InputIter1 end, InputIter2 start2, OutputIter result, Func BinaryFunc); ・形式(1)は、InputIterで示される要素を関数UnaryFuncに渡し、結果をOutputIter が示す要素に入れます。 ・形式(2)は、InputIter1で示される要素を1番目、InputIter2で示される要素を2番 目とした、2個の引数を取る関数で処理を行い、結果をOutputIterが示す要素に格 納します。 ・まずはtransform()を使ってみます。 [List1.transform()アルゴリズム使用例] ┌────────────────────────────────────┐ #include <iostream> #include <vector> #include <algorithm> using namespace std; int Power(int i) { return i*i; } int main() { vector<int> sample_int; for (int i=1; i<=10; i++) { sample_int.push_back(i); } cout << "Orijinal Value" << endl; vector<int>::iterator ip = sample_int.begin(); while (ip != sample_int.end()) { cout << " " << *ip; ip++; } cout << endl; transform(sample_int.begin(), sample_int.end(), sample_int.begin(), Power); cout << "Power 2 Value" << endl; ip = sample_int.begin(); while (ip != sample_int.end()) { cout << " " << *ip; ip++; } cout << endl; return 0; } └────────────────────────────────────┘ ┌────────────────────────────────────┐ [出力結果] Orijinal Value 1 2 3 4 5 6 7 8 9 10 Power 2 Value 1 4 9 16 25 36 49 64 81 100 └────────────────────────────────────┘ ・List1では、vectorコンテナに格納した数値をPower関数で2乗し、その結果を元の vectorコンテナに格納しています。 ・関数Power(double d)の引数には、結果から見るとコンテナの要素が渡されている ようです。これについてもう少し考えてみます。 ◆関数ポインタ ・transformアルゴリズムではどのように引数を渡しているのでしょうか。引数に記 述しているのは関数名なので、これは関数ポインタを渡しています。(*1) ・つまりList1の例では、transform()アルゴリズムの引数であるUnaryFuncにPower関 数ポインタを渡しています。恐らくtransform()は *op = UnaryFunc(*ip); // op : Outout Iteration // ip : Input Iteration のように記述されているのでしょう。2つの引数を取る場合も同様に考えることが できます。 ・関数ポインタであれば *op = (*UnaryFunc)(*ip); でも良いのか? と思われるかもしれませんが、それはNGです。理由は後述します が「敢えて」関数ポインタ的記述は避けています。 ◆関数テンプレートと実行速度への懸念 ・List1の例では、int型データを要素に持つvectorを処理していますが、その他のデ ータ型にも対応する場合を考えてみます。ここで思いつくのは関数テンプレートの 適用です。 template <class T> T Power (T obj){ return obj*obj; }; ・transform()アルゴリズムでは、以下のように呼び出しを記述します。 <int>の場合 transform(sample_int.begin(), sample_int.end(), sample_int.begin(), Power<int>); <double>の場合 transform(sample_int.begin(), sample_int.end(), sample_int.begin(), Power<double>); ・これで対応できることはわかりましたが、これらは関数なので実行毎に呼び出され ます。また渡しているのはポインタなので、インライン化のような最適化は行われ ません。 ・今、考えているのは「コンテナ要素への処理」なので、多数回の繰り返し実行に適 した形が望ましいことは自明でしょう。 ・このため、STLでは関数のように動作するクラスオブジェクトを作成することで、 インライン化可能なコードを提供し、より効率的な実行環境を実現します。それが 関数オブジェクトです。 ●関数オブジェクト ◆operator()が定義されたclass ・Power()関数の対応データ汎用化に必要なことは「型情報を渡すこと」です。それ には、transform()アルゴリズム引数に型情報を記述し、その段階で処理オブジェ クトが生成される方法を考えることになります。 ・引数に記述された段階でクラスのオブジェクトを生成するには、記述テクニックと してコンストラクタを明示的に呼び出す方法があります。 transform(sample_int.begin(), sample_int.end(), sample_int.begin(), Power<int>()); コンストラクタ明示記述 ・そしてtransform()アルゴリズム内部は *op = UnaryFunc(*ip); であることは先ほど説明しました。これに対応するため、アルゴリズムに渡すクラ スでoperator()を定義すれば良いのです。 ・この場合UnaryFuncは生成されたクラスPowerのオブジェクト自身を指します。先ほ ど「敢えて(*UnaryFunc)と記述しない」と言ったのは、クラスオブジェクトが渡さ れた場合に対応するためだったのです。これでアルゴリズム自身は関数ポインタと クラスオブジェクトの両方を実行することができます。 ・これらを考慮すると、Power()関数の機能を異なるデータ型で汎用的に実現する クラスPowerは以下のように記述できます。 template <class T> class Power { public: T operator()(T obj) { return obj*obj; } }; ・従って簡単に言えば「関数オブジェクト」とは、「operator()が定義され、関数の ように動作するクラス」となります。 ・それでは例として関数オブジェクトPowerを使用し、intとdouble両方の処理を行っ てみます。 [List2.関数オブジェクトの記述例] ┌────────────────────────────────────┐ #include <iostream> #include <vector> #include <algorithm> using namespace std; //関数オブジェクトPower template <class T> class Power { public: T operator()(T obj){ return obj*obj; } }; //出力部もついでにテンプレート化 template <class T> void DisplayElement(T& obj){ T::iterator ip = obj.begin(); while (ip != obj.end()) { cout << " " << *ip; ip++; } cout << endl; } int main () { //==== int型での実行 ==== vector<int> sample_int; for (int i=1; i<=10; i++) { sample_int.push_back(i); } cout << "Orijinal Value" << endl; DisplayElement(sample_int); transform(sample_int.begin(), sample_int.end(), sample_int.begin(), Power<int>()); cout << "Power 2 Value" << endl; DisplayElement(sample_int); //==== double型での実行 ==== vector<double> sample_double; for (double d=1.1; d<=11.0; d+=1.1) { sample_double.push_back(d); } cout << "Orijinal Value" << endl; DisplayElement(sample_double); transform(sample_double.begin(), sample_double.end(), sample_double.begin(), Power<double>()); cout << "Power 2 Value" << endl; DisplayElement(sample_double); return 0; } └────────────────────────────────────┘ ┌────────────────────────────────────┐ [出力結果] Orijinal Value 1 2 3 4 5 6 7 8 9 10 Power 2 Value 1 4 9 16 25 36 49 64 81 100 Orijinal Value 1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 11 Power 2 Value 1.21 4.84 10.89 19.36 30.25 43.56 59.29 77.44 98.01 121 └────────────────────────────────────┘ ◆関数オブジェクトの基底クラス ・これは記述のテクニックになりますが、通常、関数オブジェクトを作成する際には STLから供給される基底クラスを継承します。これにより標準的な引数型と戻値型 の名前を使用することができます。 template <class Arg, class Res> struct unary_faunction { typedef Arg argument_type; typedef Res result_type; }; template <class Arg, class Arg2, class Res> struct binary_function { typedef Arg first_argument_type; typedef Arg2 second_argument_type; typedef Res result_type; }; ・これらの基底クラスを利用するには <functional>ヘッダをインクルードする必要 があります。先ほどの関数オブジェクトPowerを、この方式に則って記述すると以 下のようになります。(*2) template <class T> class Power : public unary_function<T,T> { public: unary_function<T,T>::result_type operator() (unary_function<T,T>::argument_type obj) //operator()引数 { return obj*obj; } }; ◆組み込み関数オブジェクト ・ここまで「関数オブジェクトとは何か」を理解するために、関数オブジェクトを作 る観点から話を進めてきました。しかし、実際のところSTLを使い始めた最初のう ちは組み込みの関数オブジェクトを使うのがほとんどだと思います。 ・STLでは以下の組み込み関数オブジェクトが用意されています。これらを使用する には、<functional>ヘッダをインクルードします。各関数オブジェクトの詳細につ いてはコンパイラのドキュメントを参照下さい。 ┌────────────────────────────────────┐ [2項関数オブジェクト] : オペランドが2個 plus, minus, multiplies, divides, modules, equal_to, not_equal_to greater, greater_equal, less, less_equal, logical_and, logical_or [単項関数オブジェクト] : オペランドが1個 logical_not, negate └────────────────────────────────────┘ ・組み込み関数の使用例として、異なる2個のコンテナ間要素をgreater関数オブジェ クトで比較するコードを示します。 [List3.組み込み関数オブジェクトの使用例] ┌────────────────────────────────────┐ #include <iostream> #include <vector> #include <algorithm> #include <functional> using namespace std; int main() { vector<char> obj_vec1; vector<char> obj_vec2; vector<bool> obj_vec3; obj_vec1.push_back('B'); obj_vec2.push_back('b'); obj_vec1.push_back('u'); obj_vec2.push_back('U'); obj_vec1.push_back('M'); obj_vec2.push_back('m'); obj_vec1.push_back('p'); obj_vec2.push_back('P'); obj_vec1.push_back('S'); obj_vec2.push_back('s'); for (int i=0; i < static_cast<int>(obj_vec1.size()); i++) { obj_vec3.push_back(false); //falseで初期化 } transform(obj_vec1.begin(), obj_vec1.end(), //第1項 obj_vec2.begin(), //第2項 obj_vec3.begin(), //格納 greater<char>()); //関数オブジェクト vector<char>::iterator ip1, ip2; ip1 = obj_vec1.begin(); ip2 = obj_vec2.begin(); vector<bool>::iterator ip3; ip3 = obj_vec3.begin(); while(ip1 != obj_vec1.end()){ cout << *ip1 << " "; if (*ip3) cout << "> "; else cout << "< "; cout << *ip2 << endl; ip1++, ip2++, ip3++; //カンマ演算子 } return 0; } └────────────────────────────────────┘ ┌────────────────────────────────────┐ [出力結果] B < b u > U M < m p > P S < s └────────────────────────────────────┘ ・これでSTLの基礎編は終了です。これまでのレポートで概要は把握できたと思いま す。STLについて更に学習を進める場合、以下の観点があります。 (a)一般的なデータ型とアルゴリズムを学習し、STLでコードを具体化する (b)カスタマイズを意識しSTLのコンポーネントを更に学習する ・他の言語でアルゴリズムやデータ型を学んでいる場合、具体例についてはさらりと 読む程度で問題無いので、(b)が優先となるでしょう。 ・もしアルゴリズムについての知識が浅いと思ったら、(a)の優先度を上げて学習し た方がプログラミングの知識を幅広く習得できると思います。 ・機会があれば上記の内容もまとめていきたいと思います。 (*1)関数名は関数ポインタです。 void Func(int a); が存在する場合 Func(a); (*Func)(a); は全く同じものとなります。むしろ通常の関数記述形式が、省略された書き方になっ ているのです。 (*2)ここはコンパイラによって挙動が異なります。例えばcl(Visual C++)では template <class T> class Power : public unary_function<T,T> { public: result_type operator()(argument_type obj) //operator()引数 { return obj*obj; } }; のように記述することも可能です。ただし、コードの記述はなるべくコンパイラに依 存しない形にすべきです。 [Revision Table] |Revision |Date |Comments |----------|-----------|----------------------------------------------------- |1.00 |2003-08-25 |初版 [end] |
Copyright(C) 2003 Altmo
|
[C++ Index Top]
[Prev]
|