こんにちは。プログラマーのAです。トイロジックではゲーム開発を行っていて気づいたちょっとしたテクニックや、疑問に思って調べた情報などを、Tips という形で誰でも見られるよう共有しています。以前にもトイログに掲載したことがあるのですが、今回はその第二弾として C++ のラムダに関するネタをいくつかご紹介します。

シンプルなラムダ

ラムダは関数オブジェクトのシンタックスシュガーです。

auto my_lambda = []{}; 

上記のラムダは以下のようなクラスに変換されます。

struct Lambda
{
	void operator()() const {}
};
 
Lambda my_lambda;

 

キャプチャした場合

auto my_lambda = [x = 0] {};

キャプチャがある場合はメンバが追加されます。

struct Lambda
{
	void operator()() const {}
 
	int x;
};
 
Lambda my_lambda{0};

 

mutable を指定した場合

auto my_lambda = [x = 0]() mutable { x++; };

mutable を指定した場合は、非 const メンバ関数になります。
mutable がない場合は const メンバ関数なのでコンパイルエラーになります。

struct Lambda
{
	void operator()() { x++; }

	int x;
};
 
Lambda my_lambda{0};

 

関数ポインタへの変換

void(*p)() = [] {};

キャプチャがないラムダの場合は、関数ポインタへのキャスト演算子が定義されます(冒頭の説明では省略しました)。

struct Lambda
struct Lambda
{
	void operator()() const { do_anything(); }

	using F = void();
	operator F*() { return &do_anything; }

	static void do_anything() {}
};

void(*p)() = Lambda{};

ジェネリックラムダ

auto my_lambda = [](auto x) {} 

引数に auto を指定した場合、テンプレート関数に展開されます。

struct Lambda

{
	template<typename T>
	void operator()(T x) const {}
};

Lambda my_lambda;

 

修飾

auto my_lambda = [](auto&& x) {}

テンプレートと同様に const、volatile、参照、ポインタといった修飾ができます。

struct Lambda
{
	template<typename T>
	void operator()(T&& x) const {}
};
 
Lambda my_lambda;

 

複数の引数

auto my_lambda = [](auto&& x, auto&& y) {}

複数の引数はそれぞれ異なるテンプレート引数になります。

struct Lambda
{
	template<typename T1, typename T2>
	void operator()(T1&& x, T2&& y) const {}
};

Lambda my_lambda;

 

可変引数テンプレート

auto my_lambda = [](auto&&... x) {}

… も使用可能です。

struct Lambda
{
	template<typename... T>
	void operator()(T&&... x) const {}
};

Lambda my_lambda;

ラムダの型

コンパイラによって展開されたクラスは、全てユニークな型になります。つまり全く同じ処理のラムダでも異なる型として扱われます。

auto a = []{};
auto b = []{};
a = b;  // エラー! a と b は異なる型。

以下、ラムダを格納する方法について見ていきます。

 

std::function で扱う

同じ変数に格納するには std::function を使うしかありませんが、これはテンプレートと仮想関数と動的メモリ確保の組み合わせの実装となっているので、それなりの処理コストとなります。

std::function a = []{};
std::function b = []{};
a = b;  // OK

 

テンプレートで扱う

auto による型推論以外に、テンプレートで扱うことができます。

template<typename F>
void invoke(F&& f)
{
	f();
}

invoke([]{ printf("Hello world.\n"); });

なので、ラムダの引数にラムダを渡すこともできます。

auto invoke = [](auto&& f) {
	f();
};
 
invoke([]{ printf("Hello world.\n"); });

 

キャプチャする

キャプチャすることもできます。(VS2015あたりだとコンパイルに失敗しがちですが)

auto invoke = [f = []{ printf("Hello world.\n"); }]() {
	f();
};

invoke();

最後に

いかがだったでしょうか。今回はコンパイラがラムダをどのように変換するかをまとめてみました。シンタックシュガーであることを知っていれば、より複雑な記述も理解しやすくなるんじゃないかと思います。それでは!

著者紹介 A
1999年にプログラマとして業界入り。2007年にトイロジック入社後は『大乱闘スマッシュブラザーズX』『Happy Wars』などに参加。