C++でクロージャ
JavaScriptでクロージャを触ってみて、C++でも検証してみました。
結論から先に言うと、参照カウントのスマートポインタ(std::shared_ptrやboost::shared_ptr)を使えば出来ました。
※1/10一部修正、詳しくはコメント欄
クロージャ概要
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
これがクラスでコピーコンストラクタが複数回走ってた原因でした。