試した環境
Microsoft Visual Studio Community 2019 Version 16.4.5
本題
最近私がやらかしたミスを書き残します。
#include <functional> #include <iostream> class MyCallee { public: MyCallee() : counter_(0) {} int countUp() { return ++counter_; } private: int counter_; }; class MyCaller { public: MyCaller(MyCallee* p) { func1_ = [=]() { std::cout << p->countUp() << std::endl; }; // OK func2_ = [&]() { std::cout << p->countUp() << std::endl; }; // NG } void call1() { func1_(); } void call2() { func2_(); } private: std::function<void(void)> func1_; std::function<void(void)> func2_; }; int main() { MyCallee callee; MyCaller caller(&callee); caller.call1(); caller.call2(); }
上記のコードですが、caller.call2();
を呼ぶとアクセスエラーが起こることがあります。
callee
オブジェクト自体が生きていても、
MyCaller
コンストラクタのポインタ変数p
はコンストラクタから抜けた段階で寿命がきます。
なのでコンストラクタの外でp
を参照キャプチャした関数オブジェクトを呼び出すと寿命が尽きたp
を参照することになります。
p
の参照先のcallee
オブジェクトは生きていますが、p
自体が寿命という訳です。
ポインタの参照先の寿命を気にするあまりポインタ変数の寿命を見落としてしまうという失敗でした。
最後に実行結果を載せます。
$ Debug\Project1.exe 1 $ Release\Project1.exe 1 $ x64\Debug\Project1.exe 1 2 $ x64\Release\Project1.exe 1 2
x86ビルドでは2を出力する前にプログラムが落ちました。
x64ビルドではアクセスエラーが発生せずcaller.call2();
が処理されてます。
何ででしょう。
今までの説明が合っているか自信なくなりました。
参考
cpprefjpによると「ラムダ式がひとつ以上の変数を参照キャプチャしている場合、参照している変数の寿命が切れたあとの、ラムダ式のコピーと呼び出しの動作は未定義」とのことです。