しらいとブログ

ネットで検索してもなかなか出てこないIT情報を独自にまとめています

【C++11/14】値か関数を受け取り評価する関数

事の始まりはC++で遅延評価の必要性に駆られたことでした。

// 時間のかかる処理
int n = sum(list);

// testの中でnが使われなければ処理が無駄になる
test(n);

そこで値の代わりに関数を渡し、必要になったときに評価してもらう方法を取りました。

auto f = [&] { return sum(list); };
test(f);

このやり方だと遅延評価は実現できますが、代償として値を直接渡すことができなくなりました。

test(100); // Error
test([] { return 100; }); // OK

そこで値と関数の両方を受け取れる関数を作ることを考えました。

まず、関数のオーバーロードで2つの関数を作るやり方を試します。

template<typename F>
void test(F f)
{
	if (...)
	{
		// 関数を値に変換
		int n = f();
		
		// n を使う処理
		...
	}
}

void test(int n)
{
	// 値を関数に変換して再帰
	test([=] { return n; });
}

test(100); // OK
test([] { return 100; }); // OK

テンプレート関数と非テンプレート関数が定義された場合、非テンプレート関数の方が優先されます。これを利用して値を受け取った方で関数に変換して再帰しています。

関数を受け取った方に本来やりたかった処理を書き、必要なタイミングで関数を値に変換しています。

逆に関数を受け取った方で値に変換して再帰すると遅延評価でなくなるので注意してください。

C++11ではこのやり方でも十分通用すると思います。ですが、C++14のジェネリックラムダ式はテンプレートみたいなことはできますが、オーバーロードが使えないためこのやり方は使えません。

// ジェネリックラムダ式(引数をautoにするとテンプレートみたいになる)
auto f = [](auto g) { ... };
auto f = [](int n) { ... }; // Error(オーバーロードできない)

ジェネリックラムダ式でもやるならテンプレートで受け取った引数が値だったらその値を、関数だったらその戻り値を返す関数が必要です。

残念ながらそのような関数は標準ライブラリには無いので自分で作る必要があります。

私が作ったものがこちらになります。

#include <type_traits>

template<typename T, typename V>
auto evaluate(V v)
    -> typename std::enable_if<std::is_same<T, V>::value, T>::type
{
	return v;
}

template<typename T, typename F>
auto evaluate(F f)
    -> typename std::enable_if<!std::is_same<T, F>::value, T>::type
{
	return f();
}

引数の型が値か関数かを正しく判定させるのは難しかったので、戻り値の型と引数の型が同じだったら値、違ったら関数という前提で作りました。このコードはC++11でも動きます。

使うときは戻り値の型を指定する必要があります。使い方はこのようになります。

int a = evaluate<int>(10);
// a = 10

int b = evaluate<int>([] { return 20; });
// b = 20

これを使うと先ほどのtest関数は次のように書けます。

template<typename T>
void test(T t) {
	if (...)
	{
		// tがint型ならn = t、tがint型以外ならn = t()
		int n = evaluate(t);
		
		// n を使う処理
		...
	}
}

ジェネリックラムダ式でも使えます。

auto f = [](auto t) {
	int n = evaluate<int>(t);
};

f(100); // OK
f([] { return 100; }); // OK

多分これが一番楽だと思います。