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

この章の概要

この章の概要です。

関数オブジェクト

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

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; }

C++14 では、ラムダ式の本体に、複数の文が含まれていても、型の推測が行われるようになりました。

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

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

#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(第2章)を使うか、 引数と戻り値は分かっているので、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

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

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

また、ラムダ式を使った場合、デフォルトでは、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

ラムダ式は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

C++14 (ラムダ式の戻り値の型推論の強化)

C++14

C++14 になり、ラムダ式の戻り値の型推論が強化され、処理本体に複数の文が含まれていても、 それぞれのオペランドの型が同一であれば、推論可能になりました

VisualC++ 2013/2015/2017、clang 3.7 のいずれでも対応しています。

C++14 (ジェネリックラムダ)

C++14

C++14 になり、ラムダ式の引数の型を推測させることができるようになりました。 この機能は、ジェネリックラムダと呼ばれます。

ジェネリックラムダを使うには、引数の型のところを「auto」にします。

#include <iostream>
#include <string>

int main()
{
	auto plus = [](auto a, auto b) { return a + b; };

	std::cout << plus(3, 2) << std::endl;
	std::cout << plus(2.5, 3.1) << std::endl;
	std::cout << plus(2.5, 2) << std::endl;
	std::cout << plus(std::string("abc"), "xyz") << std::endl;
}

実行結果:

5
5.6
4.5
abcxyz

ジェネリックラムダは、生成されるクロージャをクラステンプレートにすることで実現されています。 つまり、ジェネリックラムダの引数が、クロージャのテンプレートパラメータになります。

ジェネリックラムダは、VisualC++ 2013 では対応しておらず、2015/2017 は対応しています。 clang 3.7 では使用できます。

C++14 (ラムダ式の初期化キャプチャ)

C++14

C++14 のラムダ式では、任意の式から作られた値をキャプチャできるようになりました。 この機能を、初期化キャプチャと呼びます。

初期化キャプチャは、キャプチャリストのところに、「識別子 = 式」あるいは「&識別子 = 式」のように記述します。 従来の方法と同様、前者ならコピーキャプチャ、後者なら参照キャプチャになります。

#include <iostream>

namespace {
	int getValue()
	{
		return 100;
	}
}

int main()
{
	auto f = [a = getValue()](int b) { return a * b; };

	std::cout << f(2) << std::endl;
}

実行結果:

200

そもそも、キャプチャリストに記述した内容は、クロージャのメンバ変数に対応する訳ですから、 初期化キャプチャで与えた式は、メンバ変数を初期化する式であると考えられます。 ちょうど、コンストラクタのイニシャライザリストに初期値を書くのと同じことです。

初期化キャプチャは、VisualC++ 2013 では対応しておらず、2015/2017 は対応しています。 clang 3.7 では使用できます。


練習問題

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

問題A 問題@を更に改造して、Counter をクラステンプレートにして下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/3/25 VisualC++ 2017 に対応。

'2016/12/24 「メンバポインタ」の項を追加。

'2016/12/11 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ