技術ブログとか

C/C++/C#/DirectX/Effect

ファンクタとか

久しぶりにブログを書いたものだから「続きを読む」っていう機能の存在を忘れていた。

 

そんなわけで今回はタイトル通りファンクタについて色々。

まず、C言語およびC++には関数ポインタというものがある。

そして記述方法が色々とやっかいな代物でもある。

// int型の引数を1つ受け取り、それを出力する関数hoge
void hoge(int i)
{
    cout << i << endl;
}

int main()
{
    // 引数がint型1個で戻り値がvoidの関数を指す、関数ポインタfp
    void (*fp)(int);
    // fpにhogeをセット
    fp = hoge;

    // 1が出力される
    hoge(1);
    // 2が出力される
    fp(2);
}

main関数の省略はやめた、コードが見づらい。そのうち前の記事のコードも直しておこう。

とまあこんな具合に、型と合致する関数を指し示すポインタである。

慣れれば問題ないが、慣れないうちは記述がややこしく感じるかもしれない。

// 戻り値がintの関数への、ポインタ
int (*fp)();
// 戻り値がintのポインタ型の、関数
int *func();

このように、識別子と*を囲むカッコはつけないと意味が変わってくるので忘れないこと。

まだここまでなら読めるだろう。だが、こいつを関数の戻り値に使おうとすると途端に最悪なことになる。

例として、引数にint型を1つとり、戻り値がvoidの関数を指す関数ポインタを返す関数を宣言してみよう。

// 戻り値として返される関数
void hoge(int i)
{
    cout << "hoge" << endl;
}

// hogeへのポインタを返す関数
void (*piyo())(int)
{
    return hoge;
}

ふざけているのか、とでも言いたくなる。とてもじゃないが読めたものではない。

ここで更に、とある本でも取り上げられていた標準ライブラリのsignal関数の宣言を取り上げてみよう。

void (*signal(int sig, void (*func)(int)))(int);

もはや呪文だ、唱えたところで爆発するのは私の頭ぐらいのものだが。

 

こんな感じでハードルが高めの関数ポインタだが、こいつらと似た機能をもう少しわかりやすく提供するものがある。

ここでやっとファンクタの登場だ。

まずはファンクタというものがどういうものか、int型を引数にとり、それを出力するだけのファンクタを書いてみよう。

class Func
{
public:
    void operator ()(int i)
    {
        cout << i << endl;
    }
};

int main()
{
    // Funcの実体を作成する
    Func f;
    // 1が出力される
    f(1);
}

ファンクタというのは、関数呼び出しをオーバーロードしたクラスのことを言う。

元がクラスなのでもちろん実体化して使う必要があるが、むしろこれはメリットになり得る。

クラスなので必要に応じてメンバ関数なども使うことができる。1つの関数処理に必要なものが1つのオブジェクトとして提供されるのだ。

今回定義したFuncを戻り値とする関数だったら、以下のように書ける。

Func hoge()
{
    return Func();
}

どうだろう、関数ポインタのときと比べて非常にスマートに書けている。

一応誤解の無いように言っておくが、決して関数ポインタがスマートに書けないわけではない。

typedefを使って別名をつけてやるだけで先のコードよりはスマートに書くこともできる。

// 戻り値で返される関数
void hoge(int i)
{
    cout << i << endl;
}

// void (*)(int) に IntOutFunc の別名をつける
typedef void (*IntOutFunc)(int);
IntOutFunc piyo()
{
    return hoge;
}

ファンクタと違い、関数ポインタは1つの型が複数の関数に対応する。

void (*)(int)だったら、引数がint1つで戻り値がvoidの関数であれば別に何だって良いのだ。

これがファンクタの場合だと、定義したクラス名が型名になるため、例え同じ引数同じ戻り値を持とうとも関係無い。

まあこれについてはboost::functionstd::functionを使えば解決できる問題ではあるのだが。

 

ここでひとつ、ファンクタと関数ポインタについて以下のような例をあげてみよう。

// 引数に渡された関数を呼び出すテンプレート関数
template <typename Function>
void callFunc(Function f)
{
    f(1);
}

// ファンクタ
class Hoge
{
public:
    void operator ()(int i)
    {
        cout << i << endl;
    }
};

// 普通の関数
void piyo(int i)
{
    cout << i << endl;
}

int main()
{
    // Hogeを呼び出す
    callFunc(Hoge());

    // piyoを呼び出す
    callFunc(piyo);
}

ここで注意してほしいのは、callFunc(Hoge())はHogeの()オーバーロードを呼び出しているわけではない。

callFuncにオブジェクトを渡す必要があるため、一時オブジェクトのコンストラクタを呼んでいるだけである。

それを踏まえて見てみると、ファンクタを渡した場合でも関数ポインタを渡した場合でも正しく動いている。

templateを利用することで、ファンクタも関数ポインタも似たように扱うことが可能になるわけだ。

また、ラムダ式を利用することでもっと単純に書くこともできる。

// ラムダ式を利用した呼び出し
callFunc([](int i) { cout << i << endl; });

templateとこいつらの相性はすごく良い、様々なものがすっきりと書けるようになる。

 

エフェクト作り頑張ってるのにあまり進んでないから、明日の更新ぐらいでは少しは触れるようにしたい。