Xcodeの静的解析ツールに、メモリリークの可能性を指摘されたが釈然としない
経緯
本当はパフォーマンス測定ツールを使って、プログラムが想定外に重い処理を行っていないか、確認したかった。 とりあえず、「Analyze」を起動したら、「Potential memory leak」と言われてビクビク検証している。
プログラム
分散処理インフラのpicojsonを使っている部分が指摘された。問題を切り分けるため、同様の処理を以下のように切り出した。Analyzeでは同様の警告がでることを確認済み。
#include <string> #include "picojson.h" int main(int argc, char* argv[]) { std::vector<uint8_t> js_buf; std::string js("{}"); js_buf.resize(js.size()); memcpy(js_buf.data(), js.c_str(), js.size()); for (int i = 0; i < 10; i++) { picojson::value v; std::string err; picojson::parse(v, js_buf.begin(), js_buf.end(), &err); if (!err.empty()) { std::cout << err << std::endl; return 0; } } return 0; }
指摘内容
かなり細かく指摘される。画像のように、どのメソッドの、どの条件が〜という内容まででる。
メモリリークするのか?
コピーコンストラクタで確保した領域が、開放されない可能性がある?と言いたげだが、デストラクタで開放されている。コピーオペレータでswapを使っているが問題ないように読める。試しにvalgrindを使い、動的に検証するも、やはりリークは起きなかった。 プリプロセッサの展開がうまく解析できない(ような初歩的なことは無いと思いつつ)のかと思い、一応手動で展開したものの、状況は変わらず。 コンストラクタでnewを呼び出すと、メモリ確保の失敗時にstd::bad_alloc例外が発生して、その場合デストラクタが実行されず、リークの原因になりうる。しかし、今回に限ればnewは1度しか呼ばれないため、例外が発生してデストラクタが呼ばれなくとも、そもそも確保に失敗しているためリークは発生しない…はず。 picojsonのデータはunionで格納されており、途中で中の型を記録しているtype_変数を書き換えた場合、正常なdeleteが呼ばれず、メモリリークする可能性があるので、それを指摘したかったのか?と思い、以下のコードをAnalyzerにかけるも何も言われない。解せぬ…。
#include <memory> #include <string> #include "picojson.h" class A { public: union U { int* i; char* c; }; bool is_int; U u; A(bool is_int_) : is_int(is_int_) { if (is_int) { u.i = new int; } else { u.c = new char; } } virtual ~A() { if (is_int) { delete u.i; } else { delete u.c; } } }; int main(int argc, char* argv[]) { A a(true); bool t = a.is_str; a.is_str = true; a.is_str = t; return 0; }
もう一度見直す
そもそも、コピーコンストラクタやコピーオペレータ単体で使って再現する様なものではないらしい。試しに以下のコードでは警告が出ない。
#include <iostream> #include <string> #include "picojson.h" int main(int argc, char* argv[]) { picojson::value v1; picojson::value v2(v1); v1 = v2; std::cout << v1.serialize() << std::endl; std::cout << v2.serialize() << std::endl; return 0; }
実はパース処理の別の場所かもしれないと考え、玉ねぎの皮むきのごとく、1枚1枚関数を剥いて行こうとし、偶然以下のようにしたら警告が出なかった。差分は変数vとerrがforの中にあるか、外にあるかだけ。どちらもスタック変数であり、parse関数からすれば、状態は変わらないはず。forの中に変数がある場合、スコープが1ループごとに作る→開放されるので、vとerrのコンストラクタ、デストラクタの呼ばれる回数などは違うが、それによりメモリリークの有無が変化するとは考えにくい。釈然としないまま保留。どなたかご存知でしたら教えてください。(パフォーマンスの確認をしないとだし)
#include <string> #include "picojson.h" int main(int argc, char* argv[]) { std::vector<uint8_t> js_buf; std::string js("{}"); js_buf.resize(js.size()); memcpy(js_buf.data(), js.c_str(), js.size()); picojson::value v; std::string err; for (int i = 0; i < 10; i++) { picojson::parse(v, js_buf.begin(), js_buf.end(), &err); if (!err.empty()) { std::cout << err << std::endl; return 0; } } return 0; }