先頭へ戻る

関数オブジェクト | Programming Place Plus C++編【言語解説】 第34章

Programming Place Plus トップページC++編

先頭へ戻る

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。
当サイトでは、C++14 をベースにした新C++編を作成中ですが、今のところ、C++11 以降の C++ を詳しく解説できているコンテンツはありません。

この章の概要

この章の概要です。


関数オブジェクト

関数オブジェクトとは、関数をオブジェクトにしたもののことを指します。なお、C++ では、関数オブジェクトのことをファンクタ (functor)と呼ぶこともあります。

関数がオブジェクトになっていることによって、関数は情報(データ)を持つことができる上に、変数に保存したり、他の関数の引数に渡したりできます。このような利点を持った上で、普通の関数と同様に「f()」のような形で呼び出せます。

変数に保存したり、他の関数に渡したりという用途でいえば、関数ポインタによる方法がありますが、関数ポインタは文字どおり、関数を指し示すポインタがあるだけですから、状態を表現できません。

プログラミング言語の種類によって、関数オブジェクトを実現する方法や考え方には差異がありますが、C++ では、関数呼び出しに使う()演算子をオーバーロードすることによって実現します。つまり、関数オブジェクト(たとえば obj)を用意して、obj() のように記述すれば、operator() が呼び出されるという寸法です。

実装例を挙げます。

#include <iostream>

class Counter {
public:
    Counter() : mCount(0)
    {}

    inline int operator()()
    {
        return mCount++;
    }

private:
    int mCount;
};

int main()
{
    Counter c;

    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
}

実行結果:

0
1
2

Counterクラスは、operator() の呼び出し回数を記録します。「c()」のように呼び出しを行うたびに、現在のカウンタの値を返しつつ、インクリメントを行っています。「c」を関数名のように見れば、普通の関数の呼び出しですが、必要な情報をしっかり自己管理していることが分かります。

STLアルゴリズム

関数オブジェクトを活用する例として最たるものが、STLアルゴリズムです。STLアルゴリズムは、コンテナ、イテレータと並び、STL を構成する基盤要素の1つです。

STLアルゴリズムについての詳細は、【標準ライブラリ】編で扱っていますので、そちらを参照してください。STLアルゴリズムに含まれている各関数については、第18章第24章で、関連する話題について、第25章第26章で扱っています。


メンバポインタ

STLアルゴリズムの多くは、関数や関数オブジェクトを引き渡すことで、動作を指定できるようになっています。場合によっては、メンバ関数を使用したいこともあるかもしれませんが、その場合には、以下のようにしてメンバ関数を指すポインタを取得する必要があります(staticメンバ関数の場合は、通常の関数と同じ扱いになるので、通常の関数ポインタが使えます)。

class MyClass {
public:
    void Func() {}
};

void (MyClass::*pFunc)() = &MyClass::Func;

通常の関数ポインタと違い、型名にも、取得する際にも「どのクラスの」という部分の指定が必要です。こうして得られるポインタは、「メンバへのポインタ」や単に「メンバポインタ」などと呼ばれることがあります。

ここから少し分かりづらい部分です。クラスは1つですが、そこから作られるオブジェクトは複数あり得る訳ですが、メンバポインタを取得する際に、オブジェクトの指定をしていません。そのため実際に、メンバポインタを経由して、メンバ関数を呼び出す際には、「どのオブジェクトから」の指定が必要になります。

#include <iostream>

class MyClass {
public:
    void Func()
    {
        std::cout << "call Func()" << std::endl;
    }
};

int main()
{
    void (MyClass::*pFunc)() = &MyClass::Func;

    MyClass c;
    (c.*pFunc)();   // メンバポインタを経由して、c のメンバ関数を呼び出す

    MyClass* p = &c;
    (p->*pFunc)();  // メンバポインタを経由して、p が指す先にあるオブジェクト c のメンバ関数を呼び出す
}

実行結果:

call Func()
call Func()

妙な構文ですが、それぞれ、「.*」と「->*」という演算子を用いて、メンバ関数の呼び出しを行っています。

同様のことが、メンバ変数に対しても行えます。

#include <iostream>

struct MyStruct {
    int value;
};


int main()
{
    int MyStruct::* pValue = &MyStruct::value;

    MyStruct c;
    c.*pValue = 10;   // メンバポインタを経由して、c のメンバ変数へ代入する
    std::cout << c.value << std::endl;

    MyStruct* p = &c;
    p->*pValue = 20;  // メンバポインタを経由して、p が指す先にあるオブジェクト c のメンバ変数へ代入する
    std::cout << c.value << std::endl;
}

実行結果:

10
20

C++11 (ラムダ式)

C++11

C++11 になって、関数オブジェクトを作るための簡便な方法として、ラムダ式が導入されました。ラムダ式によって、関数オブジェクトを表現するためのクラスが自動生成されます。このクラスは、クロージャと呼ばれ、クロージャから生成された関数オブジェクトを、クロージャオブジェクトと呼びます。

従来の関数オブジェクトの使い方では、実際に関数オブジェクトを使いたい箇所から離れた場所にクラス定義を用意する必要があり、わりと不便ですし、コードが分かりづらくなることがありました。ラムダ式を使うと、関数オブジェクトを使うその場所に、コードを書くことができ、自力でクラスを定義する必要もなくなります。

たとえば、std::count_if関数(【標準ライブラリ】第19章)を使って、偶数の値を持つ要素の個数を数えるプログラムを、次のように書けます。

#include <algorithm>
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v = { 0, 1, 0, 2, 3 };

    std::cout << std::count_if(std::cbegin(v), std::cend(v), [](int elem) -> bool { return elem % 2 == 0; })
              << std::endl;
}

実行結果:

3

ラムダ式の構文は、次のようになっています。

[キャプチャリスト](引数のリスト) -> 戻り値の型 { 処理 }

「-> 戻り値の型」の部分については、「処理」に記述したコードから推測可能であれば省略できます。つまり、return文が含まれていない場合は void型であると推測されます。あるいは、「処理」の内容が、return文だけで構成されていれば、そのオペランドの型から推測されます。

そのため、今回のサンプルプログラムの場合なら省略して以下のように記述できます。

[](int elem) { return elem % 2 == 0; }

「引数のリスト」は、クロージャオブジェクトを呼び出すときに渡されてくる引数です。引数がない場合は省略しても構いません

キャプチャリストは、「処理」の中で使う自動変数を指定します。たとえば、本章の冒頭のサンプルプログラムを、次のように書けます。

#include <iostream>

int main()
{
    int count = 0;
    auto c = [&count](){ return count++; };

    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
}

実行結果:

0
1
2

この場合、変数count を参照として使用できるようにしています。変数名の先頭に「&」があれば参照として、無ければコピーとして、ラムダ式の処理内で使用できます。このように、ラムダ式の外にある変数を、ラムダ式の中で使用できるようにする機能を、キャプチャといいます。

複数の変数をキャプチャしたい場合、「キャプチャリスト」の部分に、コンマ(,) で区切って並べていけば良いです。

また、すべての自動変数をまとめてキャプチャしたい場合の省略記法として、[=] と [&] があります。前者ならすべてコピーとしてキャプチャ、後者ならすべて参照としてキャプチャします。

また、ラムダ式を変数 c で受けていますが、ラムダ式が生成するクロージャオブジェクトの型名は、コンパイラが自動的に決めることになっており、プログラムを記述する段階で知る手段はありません。そのため、auto(第7章)を使うか、引数と戻り値は分かっているので、std::function(【標準ライブラリ】第25章)を使うかする必要があります。std::function を使うなら、次のようになります。

#include <iostream>
#include <functional>

int main()
{
    int count = 0;
    std::function<int()> c = [&count](){ return count++; };

    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
}

実行結果:

0
1
2

ラムダ式による表現と、従来の方法で定義する関数オブジェクトのクラスの対応関係を整理しておくと、理解しやすいと思われます。つまり、以下の関係性になっています。

ラムダ式 生成され るクラス
キャプチャリスト メンバ変数。参照 なら参照、コピーなら実体。
引数のリスト operat or() の引数のリスト
戻り値の型 opera tor() の戻り値の型
本体の処理 opera tor() の処理

また、ラムダ式を使った場合、デフォルトでは、operator() が constメンバ関数になります。そのため、ラムダ式の処理の中で、キャプチャした変数を書き換えることはできません。operator() に付く const を外す方法も用意されており、引数のリストの後ろに「mutable」と書きます

#include <iostream>

int main()
{
    int count = 0;
    auto c = [count]() mutable { return count++; };

    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
    std::cout << c() << std::endl;
}

実行結果:

0
1
2


練習問題

問題① 本章の冒頭で取り上げた関数オブジェクトの例を改造して、初期値や増分値を自由に変更できるようにしてください。

問題② 問題①をさらに改造して、Counter をクラステンプレートにしてください。


解答ページはこちら

参考リンク


更新履歴

’2018/7/13 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

’2018/4/5 VisualStudio 2013 の対応終了。

’2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

’2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

≪さらに古い更新履歴を展開する≫



前の章へ (第33章 メンバ関数テンプレート)

次の章へ (第35章 非メンバの演算子オーバーロード)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー