2007年10月25日

Visual C++の標準ライブラリとcrtdbg.h

先日のcrtdbg.hを用いたメモリリークの自動検出の記事に関連して、開発中のソフトウェアに組み込んだ際に気づいた点がある。それはcrtdbg.hを#includeしなくても、関連の関数が使えてしまうことだ。それでは非常に不気味なので色々試行錯誤をしてみると、どうやらC++の標準ライブラリであるstringの#includeに問題があり、削除してみたところ、関数が使用できなくなった。そこで原因を探るべく、Visual C++のincludeディレクトリのヘッダーファイルを開いてざっと眺めてみたところ、どうやらstringから様々なヘッダーファイルに依存した結果、最終的にcrtdbg.hを#includeしてしまっているようだ。以下に依存関係を列挙しておく。

#include <string> ← #include <istream> ← #include <ostream>
← #include <ios> ← #include <xlocnum> ← #include <streambuf>
← #include <xiosbase> ← #include <xlocale> ← #include <xdebug>

最後のヘッダーファイルであるxdebugの22行目より

 #if defined(_DEBUG)
  //省略

  #include <crtdbg.h>

マクロ名_DEBUG、つまりVisual StudioのIDEでReleaseではなくDebugでビルドした場合に、問答無用でcrtdbg.hがロードされるわけである。何が問題なのかというと、crtdbg.hではグローバルスコープのnew演算子をオーバーロードしているので、仮に別途new演算子をオーバーロードしている場合にエラーとなってしまうのである。さらに他にもistreamをロードしている標準ライブラリとして、iostreamやiomanip、ios、sstreamなど多数のライブラリがロードを行っているようである。Visual C++で標準ライブラリを用いてなお且つnew演算子をオーバーロードする場合の解決策として、直感的にはxdebugの問題箇所にメスを入れれば良いが、根本的な問題はVisual C++の標準ライブラリの実装の問題なのでスマートではない。

2007年10月24日

メモリリークの自動検出と特定の方法

C/C++では他の言語ではあまりお目にかからない、メモリアロケーションを扱うことが出来る。便利な反面、使い方のお作法を間違えると利点以上の代償を払うことになる。メモリリークが発生する大きな要因としては、確保したメモリの開放のし忘れである。Javaなどとは違いガーベージコレクタは標準ではサポートされておらず、明示的な開放の記述が必要である。
Visual C++では、crtdbg.hというヘッダーにメモリリークを検出し、デバッグログに出力を行ってくれる独自の関数が定義されている。以下に使い方や応用例を示しておく。


まず、ヘッダーをロードしている記述のある場所を以下のように書き換えを行う。

#define _CRTDBG_MAP_ALLOC

//ここに任意の#includeを記述する
//例えばwindows.hやstdio.hなど

#ifdef _DEBUG
  #include <crtdbg.h>
  #define new  ::new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

このような順番で記述を行う理由としては、まず下の方の#defineで既存のnewをオリジナルの記述へと置き換えを行っているが、これはcrtdbg.hのバグ対策用であり、new演算子を用いた場合に適切なファイル名がデバッグログに出力されない問題を修正するためのものであるが、たとえばこの記述を標準ライブラリより前に行ってしまうと、プリプロセス時に関係の無いnew演算子まで置き換えを行ってしまい、エラーを起こしてしまうからである。なお#defineで_CRTDBG_MAP_ALLOCの定義を一番先頭で行っているのには深い理由があるのだが、その解説は後日行う。

次にmain関数もしくはWinMain関数の先頭で以下の関数を呼び出す。

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
これは、プログラム終了と同時にVisual Studioのデバッグログにメモリリークの有無を出力することを指定する記述である。

以上の記述により、malloc(int);やnew int;などを行ったままでプログラムを終了すると、デバッグログにメモリリークの詳細が出力される。

ちなみに、もしGUIやDirectXを用いたソフトウェアを開発している場合に、AllocConsoleなどで確保したコンソールにメモリリークのログを出力したい場合は、WinMain関数の一番最後の行に以下の記述を行うことにより可能となる。

HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);  //標準出力のハンドルを取得
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);  //ログの出力対象をファイルハンドルに指定
_CrtSetReportFile(_CRT_WARN,hStdout);  //ワーニングの出力対象を第二引数のストリームに指定
_CrtDumpMemoryLeaks();  //実行時点での開放されていないメモリの一覧を表示


参考:
メモリ リークの検出と特定
_CRTDBG_MAP_ALLOC マクロは、正常に機能しません。

2007年09月24日

const修飾子と定数及びポインタ

Effective C++を読んでのメモ

C言語では書き換え不可という意味であったconstであるが、C++では定数として利用できるconst。それによりC++では配列の添え字として変数の利用が可能である。ここで注意したいのはC++では#defineで指定したマクロ名を定数として利用するのは好ましくない。その理由として、まず大抵のコンパイラの実装はプリプロセッサ命令として#defineで指定されたマクロ名の置き換え処理を行うので、マクロ名は隠蔽される。もしその定数が原因でコンパイルエラーが起きた際に、原因の特定が厄介になってしまうのである。またスコープの概念も#defineには適用されないのでクラスの中で定数を宣言したい場合には#defineでは無く、constを使うべきである。

ところでconst修飾子とポインタの関係について。

int num = 100;

const int *cVar1 = #  //データに対しconst (1)
int const *cVar2 = #  //データに対しconst (2)
int *const cVar3 = #  //ポインタに対しconst (3)
const int *const cVar4 = #  //ポインタとデータに対しconst (4)
int const *const cVar5 = #  //ポインタとデータに対しconst (5)

所持している参考書では(1)と(3)と(4)のスタイルが紹介されていたが、(1)に関しては(2)の形でも表すことが可能である。(2)により(4)に関しては(5)のようなスタイルも可能である。コーディングスタイルを規定する時のために頭の隅に入れておきたい。

2007年09月19日

仮想デストラクタの持つ意味

以下のコードは危険である。

#include<iostream>
using namespace std;

class Base{
  public:
  Base(){cout << "Base::Constructor" << endl;}
  ~Base(){cout << "Base::Destructor" << endl;}
};

class Derived:public Base{
  public:
  Derived(){cout << "Derived::Constructor" << endl;}
  ~Derived(){cout << "Derived::Destructor" << endl;}
};

int main(void)
{
  Base *p = new Derived();
  delete p;
}

実行結果

Base::Constructor
Derived::Constructor
Base::Destructor

基底クラスのオブジェクトとして派生クラスのオブジェクトを生成し代入しているが、delete時に派生クラスのオブジェクトのデストラクタの呼び出しが行われないのである。仮に派生クラスのメンバ変数にポインタ等が含まれており、デストラクタでそのポインタのメモリを開放する記述を行っていた場合、デストラクタが呼び出されずにメモリリークの原因となってしまう。その為、継承の可能性のあるクラス(事実上ほぼ全て)のデストラクタは仮想デストラクタにしておく必要がある。そうすることによって、派生クラスのデストラクタが呼び出され、さらに基底クラスのデストラクタの呼び出しも正常に行われるのである。

#include<iostream>
using namespace std;

class Base{
  public:
  Base(){cout << "Base::Constructor" << endl;}
  virtual ~Base(){cout << "Base::Destructor" << endl;}
};

class Derived:public Base{
  public:
  Derived(){cout << "Derived::Constructor" << endl;}
  virtual ~Derived(){cout << "Derived::Destructor" << endl;}
};

int main(void)
{
  Base *p = new Derived();
  delete p;
}

実行結果

Base::Constructor
Derived::Constructor
Derived::Destructor
Base::Destructor

ちなみに「デストラクタは仮想デストラクタにしておく必要がある」とあるが、逆に継承を望んでいるクラスのデストラクタが仮想デストラクタで無い場合は、そのクラスが継承を想定していない(継承に対しての対策が無い)設計だという可能性も考える必要がある。

NVIイディオムとは

Effective C++を読んでのメモ

NVI(non virtual interface:非仮想インターフェース)イディオムとはデザインパターンのテンプレートメソッドの具体化の一例。クラスに実装したpublicな関数(非仮想関数)からprivateな仮想関数を呼び出す設計。publicな関数がラッパ関数(Wrapper Function)となり、派生クラスで再定義された仮想関数を呼び出す仕組み。派生クラスで不変的な動作を基底クラスのpublicな関数で定義したい場合などに有効。

2007年09月13日

関数のインライン展開について

Effective C++を読んでのメモ

関数名の前にinlineキーワードをつけると、コンパイラ側の方で可能なら関数のインライン展開を行ってくれる関数定義の機能。もともとはC++における新機能であったが、C99によりC言語でも可能となった。たいていのコンパイラの実装では、関数内にforやwhileなどのループ文や再帰などの記述が含まれている場合、あるいはその関数のアドレスを関数ポインタ等を用いて呼び出している場合にインライン展開行わない。そのほか、クラス定義の中に書かれたコンストラクタやデストラクタは通常は非明示的にインライン関数とされる。しかしコンストラクタやデストラクタでループなどを用いていなくてもインライン展開されない可能性がある。その一つとしてコンパイラ側がコンストラクタ及びデストラクタの呼び出しに関数ポインタを用いる場合があるからだ。
なのでインライン展開を望む関数に対しinlineキーワードを用いる場合はその性質に注意する必要がある。

2007年09月11日

関数の引数について

Effective C++を読んでのメモ

C言語の仕様では関数の引数の評価順番は定められていない。C++でも同様なので、newで確保したオブジェクトに対してにスマートポインタを用いる際、後から評価された引数から例外が投入される可能性のある場合、メモリリークの原因となる。よってそれを防ぐためにはnewで確保する記述は独立した形が好ましい。

2007年09月01日

STL setのテンプレート引数にクラスを用いた実装例

#include <iostream>
#include <set>

#include <cstdlib>
#include <ctime>
using namespace std;

class Data{
public:
  int ID;
  int Score;
  Data(int ID,int Score):ID(ID),Score(Score){};

  bool operator<(const Data &d) const{
    return Score<d.Score;
  }
};

int main(void)
{
  multiset<Data> User;

  srand((unsigned)time(NULL));

  for(int i=0;i<20;i++)
    User.insert(Data(i,rand()%1000));

  for(multiset<Data>::iterator itr=User.begin();itr!=User.end();++itr)
    cout << (*itr).ID << "," << (*itr).Score << endl;
  return 0;
}

イテレータにおける前置と後置

インクリメント演算子のオーバーロードをする場合、一般的な後置の実装は変更前の値のバックアップの記述を行う。
よってSTLコンテナでイテレータをインクリメントする場合、前置で書いたほうがパフォーマンス的に宜しい。
特に非PODにおいては、コピーコンストラクタなどで例外が発生する恐れもあるので尚のこと。
×

この広告は90日以上新しい記事の投稿がないブログに表示されております。