CやC++の開発ではメモリリークに悩まされることが多い。メモリ管理はスマートポインタに限定するなど自分たちが注意しても、外部で開発されたコードやレガシーコードによって結局逃れられないことがしばしばある。
さらに組み込み開発といったコードの実行環境に制約が多い場合は、検出や再現がやりにくいことから、メモリリークのデバッグやテストが結構なストレスになることがある。
こうした、面倒な問題になりがちなメモリリーク対応では、全てに対応できるというわけではないけれど、ユニットテストでの検証が有効なことが多い。ユニットテストならば、再現性の確保、異常な入力の実現、コードの切り分けといったものが容易なためだ。デバッグ等で便利なので、今回いくつかの方法をまとめたいと思う。
対象のコード
今回はメモリリークを発生させる題材として、以下のコードを解析する。
class Base { }; class Hoge : public Base { int *pvalue_; public: Hoge() { pvalue_ = new int; } ~Hoge() { delete pvalue_; } };
Effective C++など数多くのC++の解説書が注意していて有名だけれど、上記のHogeクラスを以下のように使用するとメモリリークが発生する。
Base *pHoge = new Hoge(); delete pHoge;
動的解析ツール上でユニットテストを実行する
ユニットテストでメモリリークを検出する方法としては、まず単純にメモリリークを検出する動的解析ツール(例えばValgrind等)上でユニットテストを実行させる方法がある。
例えば以下の様なgoogle testのテストコードを記述する。
TEST(HogeTest, hoge) { //下記コードでメモリリークが発生 Base*pHoge = new Hoge(); delete pHoge; };
これをビルドして、以下のようにValgrind上で動かしてしまう(test_runner.outがテストの実行ファイル)。
valgrind --tool=memcheck --leak-check=yes --leak-resolution=high ./test_runner.out
すると、以下のようにメモリリーク発生をValgrindが報告してくれる。
==5155== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==5155== at 0x402B9B4: operator new(unsigned int) (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==5155== by 0x804AE38: Hoge::Hoge() (main.cpp:17) ==5155== by 0x804AC71: HogeTest_hoge_Test::TestBody() (main.cpp:29) (中略) ==5155== LEAK SUMMARY: ==5155== definitely lost: 4 bytes in 1 blocks ==5155== indirectly lost: 0 bytes in 0 blocks ==5155== possibly lost: 0 bytes in 0 blocks ==5155== still reachable: 0 bytes in 0 blocks ==5155== suppressed: 0 bytes in 0 blocks
この方法の良い所としては以下がある。
- mtraceやnewのラッピングといった特別な記述がなくともリークの検出ができる
- またValgrindに限ると、Valgrindは大変強力なツールでメモリリークの検出以外の機能も多数備えていて、それら他機能の活用もしやすくなる。
ただ欠点もある。まずCやC++のテストフレームワークはプリプロセッサで関数名を生成することが多いので、動的解析ツールのレポートからどのテストメソッドでリークが発生したかの判別がしばしば面倒になる。またValgrindとユニットテストのログが混在してしまうので、ログの切り分けや用途の切り分けが必要になる。
ただ一方で動的解析ツールで問題として挙げられがちな、処理の低速化やタイミングのズレについては、ユニットテストならば問題にならないことが多い。
なお何が良いかは環境要因も多いだろうけれど、ユニットテストによるメモリリーク検出については、自分はとりあえず上記の例のようにValgrind上でgoogle testやCppUTestを動かす方法に頼ることが多い。
環境や処理系のメモリリーク検出機能を使用する
実行環境や処理系がメモリリークの検出機能を持つものがある。その活用でユニットテスト上でのリークの検出が可能な事がある。
例えばC++向けのユニットテストフレームワークであるBoost.Testは、MSVCでビルドして実行すると、MSVCの機能を用いてデフォルトでメモリリークの検出・報告を行ってくれる。
例えば以下の様なコードを実行する
#define BOOST_TEST_MAIN #include <boost/test/included/unit_test.hpp> #include "hoge.h" BOOST_AUTO_TEST_CASE(testHoge) { Base *pHoge = new Hoge(); delete pHoge; }
するとテストレポートに以下の様なメモリリークのレポートが挿入される
Detected memory leaks! Dumping objects -> (略)
こちらはテストコード上でメモリリーク検出機能を記述できるので、その他のユニットテストと統合しやすい・レポートも見やすいといったメリットがある。
メモリリークの検証機能付きのアロケータを使用する。
前の方法と被るところもあるけれど、あとはmallocやnewなどのメモリ管理関数を、メモリリーク検証機能付きのものに置き換える方法がある。
例えば著名なmtraceはそれに該当する。あとGoogle Testのマニュアルでも、new、deleteを自前のものに置き換えてメモリリークを検証する例が紹介されている(sample10_unittest.cc - googletest - Google C++ Testing Framework - Google Project Hosting)。
この方法は、製品コードあるいはテストコードに専用のコードを記述する手間がかかる。ただ速度低下や環境制約などが少ないので、テスト環境・本番環境を区別せずに活用できるメリットが大きい。 例えばメモリリーク検出機能を持つ独自のアロケータを製品コードで使用しておけば、システムテスト中でもメモリリークの発生有無をチェックできるようになる。