読者です 読者をやめる 読者になる 読者になる

C/C++でのユニットテストによるメモリリーク検出

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)。
この方法は、製品コードあるいはテストコードに専用のコードを記述する手間がかかる。ただ速度低下や環境制約などが少ないので、テスト環境・本番環境を区別せずに活用できるメリットが大きい。 例えばメモリリーク検出機能を持つ独自のアロケータを製品コードで使用しておけば、システムテスト中でもメモリリークの発生有無をチェックできるようになる。