関数オブジェクト(前編)
 
(このコンテンツはメールマガジンの 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 など似たような物がひと通り揃っています。
 
透明
透明
■ C++ : クラステンプレート ( #06 )
 「テンプレート」は、関数だけでなくクラスにも使えます。
 例えば、#03 で紹介した「一般的なクラス」をテンプレート化すると、次のようになります。
 
template< class type_value >
class CClassDataInTemplate
{
private:
    type_value m_value;

public:
    type_value GetData()
    {
        return m_value;
    }

    void SetData( type_value p_value )
    {
        m_value = p_value;
    }
};
	
 
 書式などは、基本的には関数テンプレートの時と同じです。 template キーワードを付けて、特定の型をテンプレート引数に置き換える、という方法です。今回は int をテンプレート引数 type_value に置き換えてみました。
 このクラステンプレートの使用例を見てみましょう。
 
void Use_CClassDataInTemplate()
{
    CClassDataInTemplate<int> cClsData;
    cClsData.SetData( 100 );
    printf( "%d\n", cClsData.GetData() );
}
	
 
 重要なのは、クラステンプレート CClassDataInTemplate のあとに <int>が付いていることです。これは「テンプレート引数を指定する」ものです。
 関数テンプレートの時は、テンプレート引数を明示的に指定する必要はありませんでした。渡した引数に応じて、テンプレート引数が自動的に決定されていたからです。
 クラステンプレートの場合には、クラス名のあとに <型名> として、明示的にテンプレート引数を指定する必要があります。この指定された型(この例では int )で、テンプレート引数(この例では type_value )が置き換えられます。置き換えられたあとは、 #03 の時のクラスと同じものが、コンパイラ内で仮想的なコードとして作成され、そのクラスが使用されます。
 
 CClassDataInTemplate はテンプレート化されているので、もちろん次のような使い方もできます。
 
    CClassDataInTemplate<double> cClsData;
    cClsData.SetData( 3.1415 );
    printf( "%f\n", cClsData.GetData() );
	
 
 今度は double に置き換えてみました。これがテンプレート化のメリットです。
 クラステンプレートは、関数オブジェクトや iostream の多くのクラスで採用されています。標準 C++ ライブラリに慣れる第1歩は、テンプレートに慣れることとも言えるので、解説書を見ずに読み書きできるよう練習しておきましょう。
透明
透明
 
 さて、関数オブジェクトにどのような意味があるのでしょうか。
 関数オブジェクトは「関数ポインタやポリモーフィズムの代わり」と言えるでしょう。「ある関数の呼び出しを変えられるようにする」場合、これまでは関数ポインタやポリモーフィズムを使用していました。関数オブジェクトは、この代わりを「テンプレート」で実現しています。
 
////////////////////////////////////////////////////////////////
//    関数オブジェクトを受け取る関数。
#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() をアルゴリズムに置き換えて使用します。次はこれについて見ていきましょう。
 
透明
透明
■ C++ : 変数の作り方 ( #05 )
 C++ では、変数を作る方法がいくぶん拡張されています。この新しい方法は、見慣れていないと妙に感じるかもしれません。今のうちに見慣れておきましょう。
 一般的に使われている「変数の作成と同時に初期化する」方法は
 
    int i = 100;
	
 
でしょう。 C++ では、次の書式でも同じことができます。
 
    int i( 100 );
	
 
 まるで関数みたいですね。実際、そう考える方が C++ では自然と言えるでしょう。  同じような機能を、クラスにも装備させることができます。これを「コンストラクタ」と言います。
 
class CWithConstructor
{
public:
    int m_i;
    
    //    コンストラクタ。
    CWithConstructor( int p_i )
    {
        m_i = p_i;
    }
};
	
 
 このクラスには、戻り値のない同名のメンバ関数がありますね。この形式のメンバ関数は「コンストラクタ」とコンパイラに見なされます。
 コンストラクタを作ることで、クラス型変数を作成したときに初期化することができます。
 
    CWithConstructor cWithCon = 100;
	
 
 また、次の方法でも初期化できます。
 
    CWithConstructor cWithCon( 100 );
	
 
 これは組込型と同じですね。というより、組込型とクラス型との差を無くすために、組込型の機能を拡張したと考えた方がいいかもしれません。
 
 初期化のふたつの方法のうち、 = を使ったものは使えない場合があります。クラスのコンストラクタは引数をふたつ以上受け取ることもできます。その場合には関数形式でしか初期化できません。
 std::strstream がその例ですね。このクラスは、初期化時に「配列」「サイズ」「入出力フラグ」のみっつを受け取ります。この場合には = を使っての初期化はできません。
 
 さて、 C++ にはさらに特殊な変数宣言の方法があります。
 
void GetInt( int p_i )
{
    printf( "%d\n", p_i );
}

void Use_GetInt()
{
    GetInt( int( 100 ) );
}
	
 
 int( 100 ) 、かなり気味悪いかもしれませんね。これも C++ の正式な書式です。重要なのは、これは「 int 型変数を作っているわけではない」ということです。実は、この書式は「 GetInt() の引数 p_i を 100 で初期化する」というものなのです。
 普通に変数を作ってそれを「値渡し」で関数に渡した場合、元の変数と関数の引数とのふたつの変数が作成されます。 C++ ではそれを避ける方法として、このような書式が用意されています。どちらかと言えば「引数のコンストラクタを明示的に呼び出す方法」と考えた方がいいかもしれませんね。
 この書式は、 STL の関数オブジェクトを使用するときに多用するので、今のうちに見慣れておきましょう。
透明
透明
 
 
 関数オブジェクトの使い方 ( #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::transform() の中身  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 は、 () 演算子をオーバーロードしています。基本的に、この演算子をオーバーロードさえしていれば、関数オブジェクトになることができます。
std::transform() と std::generate() の違い  この関数オブジェクトは、引数を受け取らず、 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.