assert系デバッグ処理についてのメモ.assert(系)マクロは、主に外的要因でなくプログラム内での矛盾/バグを 検出するためのデバッグ機能。 このチェックで引っかかるバグは、プログラマに対する直接的なモノで、 プログラマ以外のスタッフは本来、頻繁に出会うはずのないエラーの類。 assertチェックは、よほどのことが無い限り、組込系の小さいものから 大規模なプロジェクトまでさまざまに使われる. ただ標準のモノだけだと不便もあるため、assertの仕様にあわせた デバッグ処理群を用意する場合もある. と、いうことで、そのへんに関して、 プリプロセッサマクロの使い方の例を兼ねて、書いてみる. (おさらい)
assertまずは基本的なこと.
開発時のデバッグ用の処理として、c/c++ 標準では、 これを include することで、assert(x) マクロが使える。これは、たとえば、
void foo(int x, bool player) { のように、成立すべき条件式を assert で記述しておく. DEBUGコンパイル時はチェックルーチンが生成され、条件を満たさない場合、 エラーログを出力&終了し、RELEASEコンパイル時は、なんらチェックせず、 1命令もコードを生成しない.
RELEASEコンパイル, DEBUGコンパイルの判定は、
通常、VC等一般的なコンパイラのプロジェクトファイルのデフォルトでは
もし、自分でコンパイル用の makefile やバッチorシェル・スクリプトを記述する場合は、状況にあわせて NDEBUG を定義、未定儀する必要がある.
assert の実装assertマクロは、例えば、以下のような感じに定義できる. #ifdef NDEBUG // NDEBUG定義時(RELESEコンパイル時)は何もしない. #define assert(x) #else // NDEBUG未定儀時(DEBUGコンパイル時)は条件をチェック. #define assert(x) do { \ if (!(x)) { \ printf("Assertion failed: %s, file %s, line %d\n" \ , #x, __FILE__, __LINE__); \ exit(1); /*abort();*/ \ } \ } while (0) #endif プリプロセッサ・マクロの記述に慣れてないと?な代物かもしれない. このマクロ定義では、マクロ記述での基本的な事が結構含まれていて
のようになっている。
※ 隣接文字列定数の連結機能を用いて
#define assert(x) (!(x) && _assert_message(#x,__FILE__,__LINE__)) のような感じに、出力&プログラムストップ処理をまとめたサブルーチンを用意しているかもしれない。(このほうが、デバッグ処理として生成されるコード量が減るだろう)
static_assertc/c++の次の標準規格(c1x, c++0x)では、
static_assert(定数(条件)式, "エラー時のメッセージ") という感じで、コンパイル時に、定数式が真(非0)であるかをチェックし、 偽(0)だったら、エラーメッセージを表示する、という機能が追加される. 例えば (試してないのでたぶん) static_assert(sizeof(int) == 4, "intが4バイトでない");
static_assert はコンパイラ自身への追加機能で実現されるが、
エラーメッセージ出力無しならば、現行の c/c++でも似たような処理を用意できる.
→定数式の結果が0のときに、コンパイルエラーになるような文を作ればいい.
#define STATIC_ASSERT(x) typedef char static_assert_check[ (x) ? 1 : -1 ]
のようにすれば、定数式 xの結果が真ならば static_assert_check という名のchar[1] の型がつくられokだが、偽だとchar[-1]でコンパイルエラーになる.
さて c++ (および c でも同様な拡張をしているコンパイラ) なら typedef は同じ内容なら複数回定義できるが、c の場合は 名前(static_assert_check)が衝突してしまう。 なので名前の衝突回避としてマクロの連結機能## と 行番号 __LINE__ を用いてその都度適当に新しい名前を生成することにする。 ダメな例を先に出すと #define STATIC_ASSERT(x) typedef char static_assert_##__LINE__[ (x) ? 1 : -1 ]
は static_assert___LINE__ というふうに先に連結##が機能して固定の名になってしまい意味なし。
いつマクロ(名) が展開されるかが問題で、連結##より先に __LINE__ が展開されるようにするにはマクロ引数の展開を利用する。 #define M_CAT(a,b) M_CAT_2(a,b) #define M_CAT_2(a,b) M_CAT_3(a##b) #define M_CAT_3(x) x 実はこのへんコンパイラごとに微妙に非互換があり、M_CAT_3が不要なコンパイラも多いけれど vc だと必要。 (また、これではダメなコンパイラもあるようだけど、手持ちにないので未考慮。具体的には boost/preprocessor 等を参照のこと) これを用いて #define STATIC_ASSERT(x) typedef char M_CAT(static_assert_,__LINE__)[ (x) ? 1 : -1 ]
のようになる。
C++の場合は (エラーメッセージを多少わかりやすくできたりするので) template を用いた実装のほうがよいかもしれない。 そのへんは BOOST_STATIC_ASSERT とか諸々あるので、それらを使うなり参考にするなり。
assert系マクロ作成assertマクロは、エラーになったとき、変数の値等までは出力しない。 デバッガ上でとめられたならば、まだ値を見れる可能性もあるが、 ターゲット環境/ユーザー環境ではログは出せるけどデバッガは使えないことも多い。 ので、例えば assert と同じような仕組みで ASSERT_LIMIT(変数名,最小値,最大値) // 変数の範囲チェックマクロ ASSERT_PTR(ポインタ名) // ポインタが正常なメモリを指しているか のようなモノを用意して、エラーログとして変数とその内容、を出せると便利になる. ということで、そういうマクロを作ってみる.
下準備 1 abort(), puts()
チェックマクロから使う基本ルーチンを用意する.
一番最初の assert マクロ例からすると printf(), exit(1)/*abort()*/ あたり。多少、複数の環境を考慮してマクロにしておく. とりあえず、コマンドラインツール向けならば
#define ERR_ABORT() exit(1) // プログラム中断. 場合によってはabort() #define ERR_PUTS(s) fputs(s, stderr) // 一行を標準エラー出力
のような感じか.
(マクロ名はとりあえず、ERR_ではじめることにする)
#define ERR_ABORT() DebugBreak() // ブレークポイント停止 #define ERR_PUTS(s) OutputDebugString(b) // デバッグ窓への出力
他のターゲット環境用の場合でも、win同様に ブレークポイントを設定できる場合はそれを、 なければ #define ERR_ABORT() (*(char*)0 = 0) のようにしておけばアドレスエラーでストップ、 デバッガ上でならば、運がよければ PC を変更して継続できるかもしれない.
ターゲットでの1行出力は、
USB,LAN,232C等でログを出せるならばそれを、
無くて書き込みメディアが使えるならそこに、
あるいは、画面表示できるならば、それでよいだろう.
(PG以外のスタッフがエラーをみることあるならば、できれば多少手間でも日本語表示も出来るようしとくほうが何かと楽)
下準備 2 : printf系
ERR_PUTS(s) 1行出力だけでは通常の利用で面倒なのでprintf形式も用意する. その準備として、まず、vsnprintfの辻褄あわせマクロを用意.(名前が微妙に違ったり、vsnprintfが無かったり) #define ERR_VSNPRINTF(b,l,f,a) vsnprintf(b,l,f,a) // 普通 #define ERR_VSNPRINTF(b,l,f,a) _vsnprintf(b,l,f,a) // win系 #define ERR_VSNPRINTF(b,l,f,a) vsprintf(b,f,a) // snprintfがない場合 とりあえずヘッダのみの利用を想定して、実処理を inline で記述(inline 自体が c だと__inline だったりするかもしれないが). static inline int err_printf(const char* fmt, ...) { char buf[1030]; va_list arg; va_start(arg,fmt); ERR_VSNPRINTF(buf, (sizeof buf), fmt, arg); va_end(arg); buf[(sizeof buf)-1] = '\0'; ERR_PUTS(buf); return 1; }
固定バッファだが、プログラム内部のデバッグメッセージ用なので、さして不都合はないとする. がバッファ溢れは怖いので、可能な限り vsnprintf系を使う. これを使ったマクロを用意. やり方は古いC(プリプロセッサ)で出来る方法と、C99以降の可変マクロを用いる方法がありえる. 古い Cでも使える方法は #define ERR_PRINTF(x) err_printf x として、使うときは ERR_PRINTF(("error : %s\n", msg));
のように 二重括弧で記述する. 可変引数マクロを使った場合は、 #define ERR_PRINTF(...) err_printf(__VA_ARGS__) で、ERR_PRINTF("error : %s\n", msg); といったところ.
※ 標準エラー出力やターゲット環境用に回りくどいことしているが、エラー出力が標準出力で問題なければ #define ERR_PRINTF(x) printf x や #define ERR_PRINTF printf とするのがてっとりばやいだろう。(ターゲット環境によっては開発用ログ出力関数が printf の場合もあるし)
下準備 3: stream系なるべく、デバッグ処理側でアロケータが使われたくないとはいえ、 ポインタのチェック以外では、少々のことなら大丈夫、という考え方もある(環境次第). (※malloc系でNULL返されてる状況では、デバッグ処理がアロケートしようとしてもハングだろうで)
なので、c++ の場合 stream 系も利用するのも手.
実装に関しては、sprintfの代用としてはベターなので strstream を用いてみる.(レガシーで将来破棄される予定だろうが、今はまだあるので).
#define ERR_STREAM(msg) do { \ char _buf_[1030]; \ std::strstream _ss_(_buf_, sizeof _buf_); \ _ss_ << msg << std::ends; \ ERR_PUTS(_buf_); \ } while (0) 使うときは
ERR_STREAM("~~~\n"); ERR_STREAM("pos=(" << x << "," << y << ")\n");
といった感じか。
下準備 4: 実メモリへのポインタのチェックグローバル変数やローカル変数、ヒープ、プログラム領域等、 通常のアプリがメモリとして指すポインタのチェックとしては、 NULLチェックだけでなく、 可能ならば範囲チェックをしたほうがいい. (I/Oとか特殊なアドレスは別枠のチェックとして考える). アドレス空間へのメモリーの割り当ては、当然、ハードごとに違いはあるけれど、 32ビットCPU以上の環境の場合、0からの数十KB,数百KB, あるいは、ケツからの数十KB,数百KBなどは、少なくとも、通常のプログラムや作業メモリーが割り当てられることは無いだろう、と考えられる. とりあえず、そうだと考え、前後16Kを非RAMと仮定として #define ERR_IS_RAM_PTR(p) ((size_t)(p) > 0x3fff && (size_t)(p) < 0xffffC000) のようなチェックマクロを用意してみる. win32 ならば 前 1MB, 後 256MBは非RAMにできるかもしれない(?) #define ERR_IS_RAM_PTR(p) ((size_t)(p) > 0xfffff && (size_t)(p) < 0xf0000000) PC環境でなく、コンシューマ機やターゲット機器の場合は、 仮想記憶をフル活用はせず、 RAMは固定範囲になっていることも多いと思われるので、 さらに、具体的な範囲を判定できる可能性が高い。 そのようにして用意した ERR_IS_RAM_PTR(p) を用いて ERR_ASSERT_PTR(p) 等を作ることになる(後述).
たとえ、アドレス空間の先頭 数キロバイトだったとしても、 NULLチェックだけではひっかからないエラー (たとえばNULLポインタでのメンバー変数のアドレス参照等)が ひっかかるようになれば、メリットだろう. (まして、範囲がかなり限定されるターゲット環境の場合は、やらないと損では?と)
アライメント・チェックC++の場合、templateを用いて基本型のアライメントをチェックすることが可能.
//基本 (アライメントはなんでもokにしとく) template<typename T> bool err_check_ptr_align(T*) { return 1; } // ポインタのポインタ. template<typename T> bool err_check_ptr_align(T** p) { return !( (std::size_t)p & (sizeof(T*)-1) ); } // intポインタの場合のアライメントチェック. template<> bool err_check_ptr_align<int>(int* p) { return !( (std::size_t)p & (sizeof(int)-1) ); } // unsigned intポインタの場合のアライメントチェック. template<> bool err_check_ptr_align<unsigned>(unsigned* p) { return !( (std::size_t)p & (sizeof(unsigned)-1) ); } といった感じに、他にもshort,long,long long の符号付無, float, double, long double も同様に実装する.
※long double は2の乗数とは限らないのでちょっと例外的. 追記: 書いておいてなんだが、手間や速度等思うと、ポインタについては範囲チェックだけでも十分、という気もする.
消えるマクロ、消えないマクロ最終的な製品のコンパイルでは、すべてのデバッグマクロの実体は消してしまうが、 開発中は、RELEASEコンパイル時でも、特殊ルートや、 部分的にチェックしたい場合もあるため、 同じ動作だけど、設定によって消えるマクロ、消えないマクロを用意 しておくほうが便利。 とりあえず、ここでは、 NDEBUG時に消えてなくなるマクロは DBG_ ではじめ、 消さないマクロは ERR_ ではじめるとする. 例えば DBG_ASSERT(x) は NDEBUG定義で消えるが、 ERR_ASSERT(x) なら残る。もちろん、大半はDBG_ASSERT(x)で書くことになる. ※すべてを2重化するのは面倒なので、実際には部分的...
チェック用のマクロ以下のようなマクロを用意してみようと考える. DBG_ASSERT(x) x が成立するか. DBG_ASSERT_PTR(p) アドレスpがRAMとしておかしくないか(NULLはNG) DBG_ASSERT_PTR0(p) アドレスpがRAMとしておかしくないか(NULLはOK) DBG_LIMIT(a, mi, ma) aが[mi,ma]の範囲に収まるか(調度mi,maも含む) DBG_EQ(a,b) aとbが等しいか DBG_LIM_I(a,mi,ma) DBG_LIMITのint専用版 DBG_EQ_I(a,b) DBG_EQのint専用版 (同様にlong long,float,double等も) 多少凝ったチェック等も用意してたこともあるが、 (たとえばポインタチェックでアライメントも指定する等)、 使っていて面倒だと、使わなくなったりおざなりな指定になったりするため 結局、そこそこ単純なのだけでいい、というのが結論。
実装例としては(面倒なんで3つだけ) #define ERR_ASSERT(x) do { \ if (!(x)) { \ ERR_PRINTF(("%s (%s): %s, failed.\n", __FILE__, __LINE__, #x)); \ ERR_ABORT(); \ } \ } while (0) #define ERR_ASSERT_PTR(p) do { \ if (!(ERR_IS_RAM_PTR(p) && err_check_ptr_align(p))) { \ ERR_PRINTF(("%s (%s): bad pointer(%s=%#p).\n" \ , __FILE__, __LINE__, #p, p)); \ ERR_ABORT(); \ } \ } while (0) #define ERR_LIMIT(a,mi,ma) do { \ if (!((mi)<=(a)&&(a)<=(ma))) { \ ERR_STREAM(__FILE__ << " (" << __LINE__ << "): " \ << #a << "=" << (a) << ", out of range[" \ << (mi) << ',' << (ma) << "]\n"); \ ERR_ABORT(); \ } \ } while (0) #ifdef NDEBUG #define DBG_ASSERT(x) #define DBG_ASSERT_PTR(p) #define DBG_LIMIT(a,mi,ma) #else #define DBG_ASSERT(x) ERR_ASSERT(x) #define DBG_ASSERT_PTR(p) ERR_ASSERT_PTR(p) #define DBG_LIMIT(a,mi,ma) ERR_LIMIT(a,mi,ma) #endif
上記は、ERR_ABORT() がvoid関数であることを前提にしているが inline int ERR_ABORT2() { ERR_ABORT(); return 1; }
のようにintを返すように定義でもしておけば、 #define ERR_ASSERT(x) (!(x) && (ERR_PRINTF(("%s (%s): %s, failed.\n" \ , __FILE__, __LINE__, #x)), ERR_ABORT2() ) ) のようにもかける. 生成サイズ的にはさして違いはないだろうけど、記述量的にはシンプルになる. が、if 文の形のほうがまだわかりやすい、と感じる人も多そうな気はする.
※ void exit(int); の場合 #define ERR_ABORT2() (((int (*)(int))exit)( 1 ), 1) のように無理やりキャストするのも手……だが実際の定義では __cdecl やら __declspec __attribute__ の指定もからんでコンパイラごとの違いが面倒かもでinline関数するのが楽かも、と。
タグジャンプ
先のマクロでのエラーメッセージは、すべて
dos/win系のテキストエディタでは、この形式でかかれたテキストを開いて また MS Visual Studio のログ出力窓においても、この形式でかかれた行をマウスでダブルクリックすることで、そのソースファイルのその行に移動できる。 なので、この形式でエラー吐くのがwin系ではベターだろう.
が、面倒なことにデフォルトの assert は、この形式になっていないことが多い。 assertチェックにひっかかったとき、多少面倒なので、可能ならば assertマクロ を再定義して乗っ取ってしまうのも手. 一定規模以上のプログラムの場合、共通ヘッダ(あるいはプリコンパイルヘッダ) を用意するだろうで、その中で、#include <assert.h>のあとで、 #undef assert #define assert(x) DBG_ASSERT(x) のように再定義する. 運がよければそれで ok... が、コンパイラによっては#include <assert.h> をするたびに assert マクロが再定義(undef&define)されるものもあるようで(vc)、その場合は、個別の assert.h のincludeを可能な限り #ifndef assert #include <assert.h> #endif のような感じに assert 定義の有無をチェックするようにすればマシになるかもしれない.
__func__C99以降対応の(あるいは一部を取り入れた)コンパイラでは __func__ に、現在実行中の関数の名前が入っている. これを利用してデバッグ出力で、 DBG_PRINTF(("%s (%d): %s: ...\n", __FILE__, __LINE__, __func__ )); のように関数名も一緒に出力できる. ソース行数が変動することを思えば関数名がわかるのは有益なことが多いだろう. ただ vc等 C99対応を見送ったコンパイラでは __func__ 対応していないものもあり、規格化以前からある各コンパイラの同様の機能を判別して使うことになる。 vc,gcc系では __FUNCTION__ の名で使えるので、こちらのほうが無難かもしれない。 C++ の場合、クラス情報無しのメンバー関数名のみだったり、コンストラクタ等は簡易な名前だったり等、コンパイラによって名前がどうなるか、ばらつきが多く、紛らわしいだけで役たたずになる場合もある.
なので、コンパイラを判別して使う手間や、出力文字数(レイアウト)、等鑑みて、やっぱり関数名文字列を使わない、という選択もあるだろう. ※関数名だけの __func__,__FUNCTION__ とは別に関数定義を文字列化した名前を別途用意しているコンパイラもある。g++ だと __PRETTY_FUNCTION__、 vcだと __FUNCSIG__ 。(ついでに vc では__FUNCDNAME__ でマングル化された名前文字列になる模様)
なお、__func__ はプリプロセッサの文字列でなくコンパイラ側が用意する文字列なのでマクロで連結等はできない.
2011-07 static_assert関係を再再修正… やっぱりCだと定義内容が同じでも同名のtypedefは何度でもできなかった(c++ およびvc拡張だった). 間抜けすぎでした。 2013-06 vcでは __func__ に対応していなかったことを追記. 普段は使わないか辻褄合わせしていたようで失念していました. [追加修正] __FUNCDNAME__ は __func__ と同じでなくマングル化された名前だった. |