C++11から導入されたラムダ式はとても便利な機能ですが、その使われ方から不正な参照いわゆるダングリングポインタが発生しやすい機能でもあります。今回はその回避策を考えてみます。
- なぜ不正な参照(ダングリングポインタ)が発生しやすいのか
- 不正な参照の発生例
- 弱参照による回避策
- 強参照(std::shared_ptr)による回避策
- プリミティブ型への参照(ポインタ)の不正アクセス対策
- まとめ
なぜ不正な参照(ダングリングポインタ)が発生しやすいのか
ラムダ式は変数をキャプチャした上で、その呼び出しを遅延できるからです。C++のポインタや参照は、その参照先オブジェクトの寿命が切れることで簡単に不正な参照(ダングリングポインタ)となってしまします。ラムダ式は呼び出しをいくらでも遅延できるため、その間にキャプチャしたポインタや参照の指す先のオブジェクトの寿命が切れてしまい、不正な参照が発生しやすくなっています。
また、ラムダ式内では見た目上はキャプチャした変数のスコープ内かのように見えてしまうのも、不正な参照が発生しやすい原因かもしれません。
{ int a = 0; std::function<void()> func = [&](){ a = 10; // ↑このaはちゃんと寿命内のスコープにいるように見えるが、 // このラムダ式が呼び出されたときには寿命外のスコープにいるかもしれない }; }
不正な参照の発生例
#include <stdio.h> #include <functional> class A { int m_ID = 777; public: void Print( const char* p_mes ) const { printf( "[ID:%d]%s\n", m_ID, p_mes ); } }; int main() { std::function<void()> func; { A* pA = new A; func = [=](){ if ( pA == nullptr ){ return; } pA->Print( "Lambda" ); }; // pAインスタンス削除 delete pA; pA = nullptr; } func(); // 不正な参照が行われる return 0; }
- A型インスタンスへのポインタである
pA
をラムダ式でコピーキャプチャ([=]
)し、std::function
に格納 - その
std::function
を呼び出す前にA型インスタンスを削除。pA
にもnullptr
をセット - しかし、コピーキャプチャなのでキャプチャされた
pA
はnullptr
にはならず std::function
を呼び出し、そのまま不正にアクセスしてしまう
この例では、キャプチャ元のpA
にnullptr
をセットしたものの、コピーキャプチャ([=]
)なためキャプチャの方のpA
には反映されずそのまま不正アクセスになっています。それではコピーキャプチャではなく、参照キャプチャ([&]
)にすれば不正なアクセスを回避できるのでしょうか。答えはNOです。たしかに、参照キャプチャにすればキャプチャ元のpA
にnullptr
をセットすることで、キャプチャの方のpA
もnullptr
になります。しかし、そのあとのstd::function
を呼び出すときには、キャプチャの方のpA
の指す先ではなく、キャプチャの方のpA
自体が不正な参照になってしまいます。なぜなら、キャプチャ元のpA
の寿命が切れているからです。
このように、ポインタや参照キャプチャは参照元の破棄(寿命切れ)を検知できないため、容易に不正なアクセスになってしまいます。
弱参照による回避策
先ほどの不正な参照の発生例であった、ポインタの不正なアクセス、つまりダングリングポインタについては弱参照で簡単に回避できます。弱参照といえばstd::weak_ptr
ですが、いろいろ面倒臭さがあり個人的に好きではないので今回は自作の弱参照クラスを利用します。弱参照クラスの実装については当サイトのこちらの記事をご覧ください -> 「【C++】弱参照クラスを自作する」
#include "WeakPtr.h" #include <stdio.h> #include <functional> class A { DEF_WEAK_CONTROLLER( A ); // WeakPtrを作れるようにするための仕込み int m_ID = 777; public: void Print( const char* p_mes ) const { printf( "[ID:%d]%s\n", m_ID, p_mes ); } }; int main() { std::function<void()> func; { A* pA = new A; auto weakA = pA->GetWeakPtr(); func = [=](){ if ( weakA.IsNull() ){ return; } pA->Print( "Lambda" ); }; // pAインスタンス削除 delete pA; pA = nullptr; } func(); // 弱参照により不正な参照が回避される return 0; }
変更点
- Aクラスから弱参照を生成できるように仕込み(
DEF_WEAK_CONTROLLER
) - ラムダ式の前に弱参照を変数として用意しておき、ラムダ式でキャプチャ
- ラムダ式内ではキャプチャした弱参照を利用してオブジェクトの生存確認
クラス側に弱参照のための仕込みが必要ですが、それを含めても3行のコードの追加で不正な参照の回避ができるようになりました。かなりお手軽ではないでしょうか。
ラムダ式の前に弱参照変数を用意している部分は、C++14以降なら初期化キャプチャを利用できます。
func = [=,weakA=pA->GetWeakPtr()](){ if ( weakA.IsNull() ){ return; } pA->Print( "Lambda" ); };
強参照(std::shared_ptr
)による回避策
強参照(std::shared_ptr
)でも不正な参照は回避することができますが、以下の理由であまり好きではありません。
もちろん強参照が有効な手段の場合もあります。
この辺は「【C++】弱参照のすすめ」でも紹介しています。
プリミティブ型への参照(ポインタ)の不正アクセス対策
今回紹介した弱参照による回避策は、弱参照を生成できるクラスへの参照(ポインタ)にしか利用できません。なので例えば、int型ポインタのコピーキャプチャ、あるいint型の参照キャプチャなどでは、弱参照による対策が利用できません。
int型の参照キャプチャでの不正アクセスの例
#include <stdio.h> int main() { std::function<void()> func; { int value = 10; func = [&](){ printf( "value = %d\n", value ); }; } func(); // この時点で、valueの寿命は尽きているので不正なアクセスになる return 0; }
で、ちょっと考えてみたところ、弱参照(WeakPtr)を生成するためのクラスであるWeakPtrController
を使えばそれなりに対策ができそうな気がしてきました。以下のような感じです。
#include <stdio.h> #include "WeakPtr.h" int main() { std::function<void()> func; { int value = 10; WeakPtrController<int> weakCtrl{ &value }; WeakPtr<int> weakValue = weakCtrl.GetWeakPtr(); func = [&,weakValue](){ if ( weakValue.IsNull() ){ return; } printf( "value = %d\n", value ); }; } func(); // valueと同じ寿命であるweakCtrlが破棄されることでweakValueの参照が無効化され、不正なアクセスが回避される return 0; }
ポイント
WeakPtrController<int> weakCtrl{ &value };
とすることで、プリミティブ型からもWeakPtr
が作れるようになるWeakPtrController
は対象のプリミティブ型と同じ寿命になる場所に配置するWeakPtr
はコピーキャプチャにする
実際に試してみた所ちゃんと動作したようなのでいけそうです。ただ、WeakPtr
はコピーキャプチャにしなければいけないところが忘れやすそうで危ないと思いました。WeakPtr
をコピーキャプチャにするのを忘れて参照キャプチャにしてしまうと、WeakPtr
自体の寿命切れによる不正なアクセスとなってしまいます。これはコンパイルエラーにならず、場合によってはまともに動作しているようにも見えてしまうのがやっかいです。
プリミティブ型への参照の不正アクセス対策として紹介してきましたが、この方法はプリミティブ型以外にもDEF_WEAK_CONTROLLER
マクロによる弱参照の仕込みができないクラス、例えば標準ライブラリのクラスなどでも使えそうです。
#include <stdio.h> #include <string> #include "WeakPtr.h" int main() { std::function<void()> func; { std::string value = "aaa"; WeakPtrController<std::string> weakCtrl{ &value }; WeakPtr<std::string> weakValue = weakCtrl.GetWeakPtr(); func = [&,weakValue](){ if ( weakValue.IsNull() ){ return; } printf( "value = %s\n", value ); }; } func(); // valueと同じ寿命であるweakCtrlが破棄されることでweakValueの参照が無効化され、不正なアクセスが回避される return 0; }
参照キャプチャは控えるべき
参照キャプチャはポインタのコピーキャプチャと比べても、更に罠の度合いが強い気がします。なので、できることなら参照キャプチャは極力避けてた方がいいのではないかと思います。参照キャプチャを使うとしても[&]
によるデフォルトキャプチャは使うわず[&var]
による個別のキャプチャ指定を使い、さらに先程のWeakPtrController
を使った例のように参照先オブジェクトの寿命切れを検知できる仕組みを一緒に使うべきかもしれません。
参照キャプチャの利用方針をまとめると以下のとおりです。
- 確実に安全だとわかる場合以外は参照キャプチャはなるべく使わない
- どうしても必要な場合は、個別に参照キャプチャを行う(
[&]
にしない) - 参照キャプチャを使う場合は寿命切れを検知できる仕組みを合わせて使う
先ほどの例もよく考えたら参照キャプチャする必要ありませんね。
#include <stdio.h> #include "WeakPtr.h" int main() { std::function<void()> func; { int value = 10; WeakPtrController<int> weakCtrl{ &value }; WeakPtr<int> weakValue = weakCtrl.GetWeakPtr(); func = [=](){ if ( weakValue.IsNull() ){ return; } printf( "value = %d\n", *weakValue ); }; } func(); // valueと同じ寿命であるweakCtrlが破棄されることでweakValueの参照が無効化され、不正なアクセスが回避される return 0; }
まとめ
- 弱参照を使おう
- 参照キャプチャは控えよう