Hatena::ブログ(Diary)

wordiの日記 このページをアンテナに追加 RSSフィード

2005 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11| 12|
2006 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11| 12|
2007 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11| 12|
2009 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11| 12|
2010 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11| 12|
2011 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11| 12|
2012 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11| 12|
2013 | 01| 02| 03| 04| 05| 06| 07| 08| 09| 10| 11|
iPhone プログラミング D言語 日記 ゲーム 仕事 出来事 ねた ソフトウェア 囲碁
 | 

2012-01-08

[]C++クロージャ C++でクロージャを含むブックマーク C++でクロージャのブックマークコメント

JavaScriptクロージャを触ってみて、C++でも検証してみました。

結論から先に言うと、参照カウントのスマートポインタ(std::shared_ptrやboost::shared_ptr)を使えば出来ました。

※1/10一部修正、詳しくはコメント欄

クロージャ概要

JavaScriptサンプルソース

var counter = function(n) {
	return function() {
		return ++n;
	};
};

window.onload = function() {
	var hoge = counter(0);
	var foo = counter(10);
	
	document.writeln(hoge() + '<br>');
	document.writeln(hoge() + '<br>');
	document.writeln(hoge() + '<br>');
	
	document.writeln(foo() + '<br>');
	document.writeln(foo() + '<br>');
}

実行結果

1
2
3
11
12

counter()関数はインクリメントされたnを返す関数を返します、この引数nはcounter()関数が呼ばれて関数がreturnされた時にはスコープから外れているはずなんですが、束縛された関数hogeやfooのスコープが外れるまで延命されています。


C++で書くとどうなるか

C++11でラムダ式boost::shared_ptrを使って書き、VC++2010とgcc 4.6.1(MinGW)でビルドして実行できるのを確認しました。

#include <iostream>
#include <functional>

#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>

std::function<int()> counter(int n)
{
	//boost::shared_ptr<int> value(new int(n));
	auto value = boost::make_shared<int>(n);
	return [=]() -> int {
		return ++(*value);
	};
}

int main()
{
	auto hoge = counter(0);
	auto foo = counter(10);
	
	std::cout << hoge() << std::endl;
	std::cout << hoge() << std::endl;
	std::cout << hoge() << std::endl;

	std::cout << foo() << std::endl;
	std::cout << foo() << std::endl;

	return 0;
}

実行結果

1
2
3
11
12

JavaScriptの時と同じ実行結果になりました、

しかし何故これで動くのか不思議な気がします、ここでラムダ式の外部変数のキャプチャ方法と変数の生存期間についてもう少し突っ込んでみようと思います。


ラムダ式の外部変数のキャプチャ方法

外部変数をキャプチャする時は値コピーと参照が使えますが、値コピーはラムダ式を生成した時点で値コピーされ、ラムダ式内で読取専用の隠しメンバとして使えるようになり、参照は文字通り外部の変数を参照します。

#include <iostream>

int main()
{
	int n = 0;
	auto ref = [&]() {
		// nはmain()関数のnを参照している
		n++;
		std::cout << "ref() n:" << n << std::endl;
	};
	auto copy = [=]() {
		// nはこのラムダ式内用に値コピーされている
		// ラムダ式はconst修飾されている
		//n++; // ← read onlyの為出来ない
		std::cout << "copy() n:" << n << std::endl;
	};

	ref();
	ref();
	copy();
	copy();

	n = 10;

	ref();
	ref();
	copy();
	copy();

	return 0;
}

実行結果

ref() n:1
ref() n:2
copy() n:0
copy() n:0
ref() n:11
ref() n:12
copy() n:0
copy() n:0

ref()は参照、copy()は値コピーをしています、途中でnに10を代入していますが、ref()は実行するたびに参照している為代入が反映され、copy()は関数を作った時点で値コピーが完了している為反映されていません。


ラムダ式の外部変数のキャプチャ時の生存期間

C++では生ポインタを使うのでなければ、スコープから外れた時に自動的に開放されます、これはキャプチャ時も同じです。

下記サンプルでは変数の開放タイミングや値コピーが分かりやすいようにクラスを使っています。

#include <iostream>
#include <functional>

class MyClass
{
private:
	int copyCount; // コピーコンストラクタが呼ばれた回数を記録する
public:
	MyClass() : copyCount(0) { std::cout << "MyClass() copyCount:" << copyCount << std::endl; }
	MyClass(const MyClass &my) : copyCount(my.copyCount + 1) { std::cout << "MyClass(const MyClass &) copyCount:" << copyCount << std::endl; }
	~MyClass() { std::cout << "~MyClass() copyCount:" << copyCount << std::endl; }
	void Hoge() const { std::cout << "Hoge()" << std::endl; }
};

int main()
{
	std::cout << "program start" << std::endl;

	std::cout << "{" << std::endl;
	{

		std::function<void()> ref;
		std::function<void()> copy;
		std::cout << "  {" << std::endl;
		{
			MyClass my;
			std::cout << "ref()関数作成" << std::endl;
			ref = [&]() {
				std::cout << "ref()::";
				my.Hoge();
			};
			std::cout << "copy()関数作成" << std::endl;
			copy = [=]() {
				// myはこのラムダ式内用に値コピーされている
				// ラムダ式はconst修飾されている
				std::cout << "copy()::";
				my.Hoge();
			};
			std::cout << "関数作成完了" << std::endl;

			ref();
			copy();
		}
		std::cout << "  }" << std::endl;

		//ref(); // ← オブジェクトmyは開放されている為アクセス出来ない
		copy();
	}
	std::cout << "}" << std::endl;

	std::cout << "program end" << std::endl;

	return 0;
}

実行結果

program start
{
  {
MyClass() copyCount:0
ref()関数作成
copy()関数作成
MyClass(const MyClass &) copyCount:1
MyClass(const MyClass &) copyCount:2
MyClass(const MyClass &) copyCount:3
MyClass(const MyClass &) copyCount:4
~MyClass() copyCount:3
~MyClass() copyCount:2
~MyClass() copyCount:1
関数作成完了
ref()::Hoge()
copy()::Hoge()
~MyClass() copyCount:0
  }
copy()::Hoge()
~MyClass() copyCount:4
}
program end

VC++2010では何故か値コピーが4回実行されています(gccでは3回)、

※原因わかりました、詳しくは追記を参照

しかしそれ以外は予想通りで、myオブジェクトはスコープから外れた時点で開放され、値コピーされたmyオブジェクトも束縛元であるcopy()関数が開放された時に開放されています。


shared_ptrを使うとどうなるか

上記二つのサンプルを見ましたが、クロージャを使う為には、読取専用では「ない」外部の変数をキャプチャし、束縛した関数がスコープから外れるまで延命する必要があります、値コピーのキャプチャと参照のキャプチャの良いとこ取りな動作が必要ですがどうしましょう、ってなわけでshared_ptrが登場します。

#include <iostream>
#include <functional>

#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>

class MyClass
{
public:
	MyClass() { std::cout << "MyClass()" << std::endl; }
	~MyClass() { std::cout << "~MyClass()" << std::endl; }
	void Hoge() { std::cout << "Hoge()" << std::endl; }
};

int main()
{
	std::cout << "program start" << std::endl;
	std::cout << "{" << std::endl;
	{

		std::function<void()> foo;
		std::cout << "  {" << std::endl;
		{
			//boost::shared_ptr<MyClass> my(new MyClass());
			auto my = boost::make_shared<MyClass>();
			std::cout << "f()関数作成" << std::endl;
			foo = [=]() {
				std::cout << "foo()::";
				my->Hoge();
			};
			std::cout << "関数作成完了" << std::endl;

			foo();
		}
		std::cout << "  }" << std::endl;

		foo();
	}
	std::cout << "}" << std::endl;
	std::cout << "program end" << std::endl;

	return 0;
}

実行結果

program start
{
  {
MyClass()
f()関数作成
関数作成完了
foo()::Hoge()
  }
foo()::Hoge()
~MyClass()
}
program end

shared_ptrは参照カウントの為、すべての参照元がスコープから外れた時点で初めて開放されます、なのでキャプチャしたい変数をshared_ptrにして値コピーしておけば、大本がスコープから外れてもキャプチャ側がshared_ptrを持っている為開放されずにアクセスでき、キャプチャ側が開放されればshared_ptrもスコープから外れて開放されるためメモリリークの心配も無く安心です。


実行時にはクロージャとして振る舞えてますが、中の仕組みはC++のままで動いてます、なのでRAIIな設計を保ったままクロージャも使えます、やったね!


追記

キャプチャするラムダ式が2つ以上ならshared_ptrでもいいですが、1つでキャプチャする変数もintやdoubleなどの基本型の場合はオーバースペックです、というのもラムダ式の実態は関数オブジェクトで値コピーの時はキャプチャ対象をメンバとしてコピーしています。

int n;
auto hoge = [n]() { std::cout << "hoge() n:" << n << std::endl; }

int n;
class F {
	int n;
public:
	F(int n) : n(n) {}
	F(F && other) : n(static_cast<int&&>(other.n)) {}
	F &operator=(const F&) = delete;

	void operator()() const
	{
		std::cout << "hoge() n:" << n << std::endl;
	}

};
auto hoge = F(n);

とも書けるんですね、なのでキャプチャ箇所が1箇所でキャプチャ対象が基本型ならmutableで関数オブジェクトconstをはずしてやればいいです。

int n;
auto hoge = [n]() mutable {
	n++; // ← 出来る
	std::cout << "hoge() n:" << n << std::endl;
}

参考

http://d.hatena.ne.jp/faith_and_brave/20081211/1228989087


これがクラスでコピーコンストラクタが複数回走ってた原因でした。

gintenlabogintenlabo 2012/01/09 23:00 複数の箇所から使い回さない場合には
std::function<int()> counter(int n) {
return [n]() mutable -> int {
return ++n;
};
}
で十分な気もします.
あと new より make_shared を使ったほうが良いですよー.

wordiwordi 2012/01/10 01:25 こんな書き方が出来るんですね
調べてみたらキャプチャされたメンバは非constでラムダ式自体がconstな関数オブジェクト何ですね。。。
で、メンバにmutableを指定していると、なるほです。
情報ありがとうございます。
> あと new より make_shared を使ったほうが良いですよー.
みたいですね、書き直します。

gintenlabogintenlabo 2012/01/10 02:04 別に mutable なラムダはメンバ変数に mutable を付けてるわけじゃないですよー.
通常の( mutable でない)ラムダ式は, operator() が const 修飾されている(のでキャプチャされた変数は変更できない)のですが,
mutable を付けたラムダ式は operator() が const 修飾されていないので,キャプチャした変数を変更できる(代わりに const 参照経由で operator() を呼び出せない)ってことです.

wordiwordi 2012/01/10 08:14 ああ、なるほど。。。

 | 
Connection: close