VCのデバッグ機能

せっかくだからまとめておこう。基本的にcrtdbg.h(CRunTime DeBuG)をインクルードすると使えるようになる。


_DEBUG NDEBUG マクロ
_DEBUGはDebugビルドのとき、NDEBUGはReleaseビルドのとき、勝手に#defineしてくれます。デバッグ系の機能はこれでON/OFFをスイッチするようになっています。NDEBUGはC言語標準のもので、#include のassert()関数と一緒に使います。_DEBUGは独自拡張で、VCのデバッグ機能はこちらでスイッチします。
使用例

#ifndef _DEBUG
	dx9lib::DisableAero();
#endif

Aero切るコード入れたらテスト実行の度に重くてうっとうしいのでDebugモードでは切らないことにしました。テストに遅延とかどうでもいいから!


アサーション
NDEBUGとassert()の標準C機能は標準ライブラリの仕様に従って普通に使えます。crtdbg.hには_ASSERTと_ASSERTEマクロ関数があります。引数には真でなければならないはずの式を書きます。実行時、それが偽になった場合死んでくれます。_ASSERTEはその失敗した式も一緒に出力してくれます(マクロ関数の引数は#をつけるとダブルクォーテーションで括れる)。__FILE__や__LINE__を活用してそのアサーションのファイル名・位置も一緒に出力してくれます。
_ASSERT系は_DEBUGマクロが定義されているかどうかでスイッチし、定義されていない場合はプリプロセッサによって消えてなくなります。よって副作用のある式を書くとデバッグバージョンとリリースバージョンで動作に違いが出てしまうため、やってはいけません。
いやアサーションって原理自体はこれほどのものはないってくらい簡単だからここにくどくど書くようなものでもないか。むしろ使ってない人がどうやって使うようになるかだな。どんなゴミ環境でもアサーションの仕組みを自作するのは可能なはず。

switch (a) {
case ENUM1:
  // ...
default:
  _ASSERT(0);
}

例えばswitchで必ずどれかのラベルに飛ぶはずの場合、偽の定数をアサーションしてやると安心です。注意したいのは、例えばファイルが見つからなかったときにアサーションを失敗させてはならないということです。アサーションにはプログラムが正しいならば絶対に真になるはずの式を書きます。ファイルが無い時にすることはエラー処理、例外処理です。ファイルがないっていうのはプログラムが間違ってるってことじゃないからね。
アサーションはロジックエラーだとかセマンティックエラーだとか呼ばれる「文法上は合ってるんだけど想定した動作をしてくれない、意味上でおかしい」バグ(というかバグってほぼ100%これ…)への強力な対処になります。1回自分の置いたアサーションに引っ掛かってみれば…!


メモリリーク報告
スマートポインタがますます身近になってきた今日この頃、みなさんいかがお過ごしでしょうか。なんとプログラム終了時に解放(freeとかdeleteとか)されていないメモリ領域があった場合報告する機能があります。すべてのデストラクタがしっかり働いていることを確認するため、私はいつも使っています。
まずなるべく上の方にこれを書きます。私はプリコンパイルドヘッダに書いてます。なんか#includeの順番とか変えちゃいけないらしいよ。

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

WinMainの最初とかに

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

と書きます。おわり。これでプログラム終了時にメモリリークがあると「出力」ウィンドウに報告されます。mallocでもnewでも大丈夫です。あ、GlobalAllocとかは勘弁してください。Cランタイムじゃないんで…。

int *p = new int;
// そのまま放置

あと、リークの原因となったメモリをnewした場所のファイル名・行番号を出す機能ですが、調べるとなんか昔のバージョンでひどいバグがあって自分で#defineとかやってるのを見かけるのですが、newして放置してもファイル名とか一切出ないのでどうなったのか、直ったんじゃないのかとヘッダファイルを調べてみたら、

#if defined(_CRTDBG_MAP_ALLOC) && defined(_CRTDBG_MAP_ALLOC_NEW)
/* We keep these inlines for back compatibility only;
 * the operator new defined in the debug libraries already calls _malloc_dbg,
 * thus enabling the debug heap allocation functionalities.
 *
 * These inlines do not add any information, due that __FILE__ is expanded
 * to "crtdbg.h", which is not very helpful to the user.
 * 
 * The user will need to define _CRTDBG_MAP_ALLOC_NEW in addition to
 * _CRTDBG_MAP_ALLOC to enable these inlines.
 */

_Ret_bytecap_(_Size) inline void * __CRTDECL operator new(size_t _Size)
        { return ::operator new(_Size, _NORMAL_BLOCK, __FILE__, __LINE__); }

_Ret_bytecap_(_Size) inline void* __CRTDECL operator new[](size_t _Size)
        { return ::operator new[](_Size, _NORMAL_BLOCK, __FILE__, __LINE__); }

#endif  /* _CRTDBG_MAP_ALLOC && _CRTDBG_MAP_ALLOC_NEW */

ぱっと見_CRTDBG_MAP_ALLOC_NEWも定義してやれば使えるようになるのかと思いきや、
「これは後方互換性のためだけに残してあります」→!?
「operator newは_malloc_dbgを呼んでいるのでデバッグ機能は使えます」→うん、でもファイル名と行番号出ないね。
「このinline関数は何も情報を追加しません、なぜなら__FILE__は"crtdbg.h"に展開され、そんなものユーザーの役には立たないからです」→そこらじゅうに書かれてたけどこれひどいよね。テストしてないってレベルじゃないよね。なにが"which is not very helpful"なんだよ。
「_CRTDBG_MAP_ALLOCに加えて_CRTDBG_MAP_ALLOC_NEWを定義すればこれらのinline関数を有効化できます」→有効化してどうすんだよ…。
というわけで、デバッグ用newにファイル名と行番号をつけたい人は古いバージョンでも一緒なのでどこかからマクロで定義されたものを拾ってくるか、自分で考えてください。__FILE__や__LINE__使うんだからinline関数はマクロ関数の上位互換にはなりません><
後方互換性ってこんな何の役にも立たない関数使ってる過去のコードなんか存在しないだろ…?


DebugBreak()
これは普通のWin32APIです。_DEBUGとかは関係ありません。デバッガのブレークポイントを発生させます。デバッガつきでポインタアクセスをミスったときに出るアレっぽいのを発生させます。やれば分かる。使い道は自作アサートでアサーション失敗したときの処理とか。デバッグバージョンでのみ、例外を投げる直前にこれを呼ぶようにしておくと、「中断」を押してその時の関数の呼び出し履歴や変数の値がデバッガから見れるのでいいかも。やってないけど。デバッガがついてないと普通に強制終了します。ログだけ吐いて黙って強制終了しちゃうよりも、異常終了のダイアログを自動的に出しながら死んだ方がユーザーに分かりやすいかも(爆笑)。


OutputDebugString(LPCTSTR lpOutputString)
これもWin32APIです。デバッガに文字列を出力します。要はVCだと「出力」タブです。ただ、文字列しか出力できないため、printfデバッグよりしょぼい。まあ、printf機能とか、Releaseでは消すとか、あとなんだかんだいって出力にはDLLを読み込みましたとかいうのがいっぱい出て読みにくいってことでファイルに書き出して後でゆっくり読みたくね?とか、まあこの辺は各自の判断で。変に凝ったものを作るよりは本命のプログラムを書き進めた方がいいと思うよ(爆笑)。