C++ 11 ラムダ式とは何か

はじめに

C++ 11 で各種言語にある無名関数を C++ でも使うことができるようになりました
この無名関数のための新たな仕組みをラムダ(lambda)、正確にはラムダ式(lambda expressions)といいます

ラムダ式は無名の関数オブジェクトを作るための構文で、その背後にある仕組みも使うのもとても簡単です
しかし、これは C++ に何か新しいものが追加されたということではなく、今までもあった関数オブジェクトをより使いやすくするために新しい構文が追加されたというだけのことです

ここでは C++ 11 のラムダ式とは何者なのかということについて簡単に説明しましょう

標準では

標準では次のように言っています

N3797

5 Expressions
5.1 Primary expressions
5.1.2 Lambda expressions

1 Lambda expressions provide a concise way to create simple function objects.
2 The evaluation of a lambda-expression results in a prvalue temporary (12.2).
  This temporary is called the closure object.
  A lambda-expression shall not appear in an unevaluated operand (Clause 5).
  [ Note:
    A closure object behaves like a function object (20.9).
  -end note ]
3 The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed nonunion class type
  - called the closure type
  - whose properties are described below.
  This class type is not an aggregate (8.5.1).
  The closure type is declared in the smallest block scope, class scope, or namespace scope that contains the corresponding lambda-expression.
  [ Note:
    This determines the set of namespaces and classes associated with the closure type (3.4.2).
    The parameter types of a lambda-declarator do not affect these associated namespaces and classes.
  -end note ]
  An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:
  - the size and/or alignment of the closure type,
  - whether the closure type is trivially copyable (Clause 9),
  - whether the closure type is a standard-layout class (Clause 9), or
  - whether the closure type is a POD class (Clause 9).
  An implementation shall not add members of rvalue reference type to the closure type.
  1. ラムダ式は関数オブジェクトを作るためにある
  2. ラムダ式クロージャオブジェクトという関数オブジェクトのように振舞う一時オブジェクトを返す
  3. クロージャオブジェクトの詳細は処理系定義である

いろいろ細かい仕様はありますが、その基本は、ただ関数オブジェクトを作ってくれるだけのものだというところです

関数オブジェクト

関数オブジェクトというのは operator() を持つクラスのインスタンスのことです

struct cfoo {
	void	operator()() const {
		debug_printf( "関数オブジェクトが呼ばれたよ\n" );
	}
};

operator() を持つクラスのインスタンスは関数のように使うことができるので関数オブジェクトといいます

cfoo	foo;
foo();

関数オブジェクトをコールバックに使うこともできます

template<typename T> inline void	invoke( T&&	callback ){
	callback();
}

invoke( cfoo());

コールバックが何だか分からないという人は コールバックの薦め も読みましょう

ラムダ式の返すもの

ラムダ式の返すものは無名の関数オブジェクトです

例えばキャプチャのない単純なものは

invoke([]{
	debug_printf( "無名関数が呼ばれたよ\n" );
});

このような簡単な型になります

struct lambda_t {
	void	operator()() const {
		debug_printf( "関数オブジェクトが呼ばれたよ\n" );
	}
};
invoke(lambda_t());

ですからラムダ式を使ってもそこでヒープからメモリを確保したり実行時に何かコストがかかったりということもありません
まったく単純な仕組みです

コピーキャプチャ

コピーキャプチャがある場合には

int	x = 3;
invoke([=]{
	debug_printf( "x = %d\n", x );
});

次のようにキャプチャされた変数をメンバに持つ関数オブジェクトが作られます

struct lambda_t {
	int	x;
	void	operator()() const {
		debug_printf( "x = %d\n", x );
	}
	lambda_t( int	x_ ):x(x_){}
};

int	x = 3;
invoke(lambda_t(x));

メンバ変数が増えるだけで他はキャプチャのないときと同様に単純です

参照キャプチャ

参照キャプチャがある場合には

int	x = 4;
invoke([&]{
	debug_printf( "x = %d\n", x );
	x = 0;
});
debug_printf( "x = %d\n", x );

次のようにキャプチャされた変数の参照をメンバに持つ関数オブジェクトが作られます

struct lambda_t {
	int*	m_captured_x;
	void	operator()() const {
		int&	x = *m_captured_x;
		debug_printf( "x = %d\n", x );
		x = 0;
	}
	lambda_t( int*	x_ ):m_captured_x(x_){}
};

int	x = 4;
invoke(lambda_t(&x));
debug_printf( "x = %d\n", x );

メンバキャプチャ

メンバ変数をキャプチャしている場合に実際にキャプチャされているものは、this です
メンバ変数自体ではありません
これはコピーキャプチャでも参照キャプチャでも同じです

struct ctest {
	int	x = 5;
	void	test(){
		invoke([=]{
			debug_printf( "x = %d\n", x );
		});
	}
};
ctest	test {};
test.test();
struct lambda_t {
	ctest*	m_captured_this;
	void	operator()() const {
		auto	x = m_captured_this->x;
		debug_printf( "x = %d\n", x );
	}
	lambda_t( ctest*	captured_ ):m_captured_this(captured_){}
};
struct ctest {
	int	x = 5;
	void	test(){
		invoke(lambda_t(this));
	}
};
ctest	test {};
test.test();

関数ポインタへの変換

何もキャプチャしていない場合は、ラムダ式の返す関数オブジェクトは常に関数ポインタへ変換することもできます

inline void	invoke_static( void	(*callback)()){
	callback();
}

invoke_static( []{
	debug_printf( "無名関数が呼ばれたよ\n" );
});

これも次のような単純な型で実現できます

struct lambda_t {
	void	operator()() const {
		callback();
	}
	static void	callback(){
		debug_printf( "関数オブジェクトが呼ばれたよ\n" );
	}
	operator decltype(&lambda_t::callback) () const {
		return	&lambda_t::callback;
	}
};
invoke_static( lambda_t());

ラムダ式の正体

ラムダ式の正体というほどのものは何もなく、ただ関数オブジェクトを返してくれる構文というだけです

そしてラムダ式の返すものはここにあるような簡単な関数オブジェクトです
これはキャプチャがあってもなくても関数ポインタに変換してもしなくても同じです
ラムダ式を使うことによる追加のコストは一切ありません
安心して無名関数の機能が利用できますね