アットウィキロゴ

Tips

Tips(C言語編)





アサートを張ろう


アサートって何?

<assert.h> もしくは <cassert> 
をインクルードすることで使えるようになります。

assert(trueであるべき条件);
と使います。条件がfalseの時、プログラムはそこで死にます。

何が便利かと言うと、プログラムが問答無用で止まるのでバグが見つけやすいというのもありますが、デバッグの時に怪しい箇所を切り分けていく事が強みです。

どう使えばいいの?

例えば、
int hoge() {
   // ここら辺でバグってる
}

hoge関数の中でプログラムが死にました。もしこのhoge関数が作業の都合上500行くらいあったら・・・?勘弁して欲しいですね。ここでアサートの出番です。
int hoge() {
    assert(p);  // pがNULLなんて有り得ない
    assert(n < 0);  // nがマイナスなんて有り得ない
    
    // ここら辺でバグってる
}
この例で言えば、pとnを事前に検査しておくことで、「//ここら辺でバグってる」辺りではpとnはほぼ無罪であると分かります。「有り得ない」をアサートでカットしていくことで、デバッグ効率が上がるのです。

但し、以下のようなアサートの入れ方には注意。

pData->hoge();
assert(pData);  // NULLはおかしい

NULLチェックなのに、その前にpDataにアクセスしています。NULLだった場合、当然NULLアクセスで死んでしまいますので、assertまで辿り着いてくれません。

if (n < 0) {
    n = 0;
}
assert(n < 0);    // マイナスはおかしい

直前でnの補正をしているのに、このアサートを置いても、引っ掛かりません。ifの前に置いておくべきです。

デバッグ効率はとことんまで追求すべきです。どんどん置いていきましょう!

アサートマクロって?


デバッグ時ではアサートは有効なのが望ましいですが、リリースでは無くしたい・・・そういう時は以下のような感じに定義しましょう。

#ifdef _DEBUG  // デバッグ時のみ定義されるマクロ
#define ASSERT(x) assert(x)  // デバッグ時にはこちらが呼ばれる
#else
#define ASSERT(x) ((void)0);  // リリース時にはこちらが呼ばれる
#endif

使うときは、ASSERT() のマクロの方を使います。デバッグ時は通常のassertのラッパーですが、リリース時には意味の無いコードに置き換わります。((void)0; は、コンパイラに怒られない為の書き方です。このコード自体に意味はありません)

アサートに限らず、リリースでカットしたいものは1枚ラップするのが定番です。

ちなみに今回のプロジェクトでは既にアサートマクロを用意しているのでそちらをお使いください。
ASSERT( Bool flag );
ASSERTMSG( Bool flag, char *errmsg );
ASSERTMSGマクロは第二引数にプログラムが死んだときに表示するメッセージを指定します。是非活用ください。


デバッグとリリースでマクロの挙動を切り替える


アサートトピックとも若干被りますが、デバッグとリリースでマクロの挙動を切り替える方法です。

#ifdef _DEBUG  // デバッグ時のみ定義されるマクロ
#define XXX(x) (デバッグ時の挙動)
#else
#define XXX(x) (リリース時の挙動)
#endif

とします。プログラマは XXX(x) というラッパーを使うことで、デバッグかリリースかを気にせず使うことが出来ます。

一番恩恵を受けるのが、アサートマクロ(アサートのトピック参照)とprintfのラッパーでしょう。

#ifdef _DEBUG
#define PRINTF(...) printf(__VA_ARGS__)
#else
#define PRINTF(...) ((void)0);
#endif

デバッグ時は通常のprintfのように振る舞い、リリース時には一切無視するようになります。printfは実はめちゃくちゃ重い処理な上、モノによっては中でメモリを確保したりするので、リリース時には必ず切るようにしましょう。

未初期化変数の中身


VisualC++のデバッグビルドでは、未初期化の変数は以下のようにセットされています。

未初期化…0xCDCDCDCD
デリート済み…0xDDDDDDDD(0xFEEEFEEE とかも)

他にもあったけど、忘れちゃいました・・・デバッグ時にこのメモリを見かけたら要注意です。

enumのアレコレ


使いこなせればとても便利なenumについてです。

#define MENU_LOADGAME   (0)
#define MENU_NEWGAME    (1)
#define MENU_CONFIG     (2)
#define MENU_MAX        (3)

というコードよりも

enum {
    MENU_LOADGAME,
    MENU_NEWGAME,
    MENU_CONFIG,
    
    MENU_MAX
};

というコードを書きましょう。defineだと、項目が増えたり入れ替わったりした時にミスが起こりやすくなります。~
また、

enum MENU {
    :
    :
};
void setMenu(MENU eMenu);
MENU getMenu();

というようにタグを付けておくと、そのenumの項目以外を受け付けない(コンパイルの時点でエラーにする)型を作ることが出来ます。例えば

MENU    eMenu = 0;

というようなコードはコンパイラがエラーにしてくれます。強引にキャストしたりするとダメですが、使いようによってはセーフティなコードになると思います。

また、以下のようなテクニックもあります。

enum MENU {
    MENU_A,
    MENU_B,
    
    MENU_MAX,
    MENU_FORCE_DWORD = 0x7fffffff
};

最後に0x7fffffffの列挙を追加することで、MENUを必ず32bitの型にすることが出来ます。これが無い場合、MENUが何bitの型になるかはコンパイラ任せになってしまう場合があります(何bitかどうか気にしない場合は構わないのですが)。今時はコンパイラオプションで強制32bitとかあるかもしれませんが、小技として覚えておくと良いかもしれません。

スタティックアサート


コンパイル時アサートとか、色々呼び方はあると思います。アサートを実行時ではなくコンパイルの時点で有効にする方法で、以下のようなコードで実装します。

#define STATIC_ASSERT(exp) typedef char _dummy[(exp) ? 1 : -1]

例えば、ある構造体のサイズが2048を越えて欲しくない時、以下のようなコードをどこかに書いておきます。

STATIC_ASSERT(sizeof(Hoge) <= 2048);

Hoge構造体が2048バイトを超えてしまった時、コンパイラがエラーを吐いて止まります。いちいちプログラムを実行しなくてもいいので、結構重宝します。

ただし、変数を使った、通常のアサートの用途には使えません。コンパイルの時点では変数の中身は当然決まっていないからです。

STATIC_ASSERT(argc == 5); // これはダメ

普通のASSERTとうまく使い分けていきましょう。

カルドセプトに見る乱数問題


XBOX360版カルドセプトで、ダイスの目が奇数・偶数交互に出るというバグが見つかりました。

世間では「そんな乱数を作るほうが難しいだろう」や「有り得ないだろ!」というような意見が多く見られますが、実はそんなことありません。試しに以下のコードを実行してみて下さい。プラットフォームは何でも構いません。

#include <stdio.h>

// 超シンプルな線形合同法
static long x = 1;
void srand(unsigned s) { x = s; }
long rand() { x = x * 1103515245+12345; return x&2147483647; }

// 実行
void main()
{
	// 1000回実行
	for (int i=0; i<1000; i++) {
		int rand_result = rand() % 6;
		printf("%d (%s)\n", rand_result, ((rand_result&0x1)?"奇数":"偶数   "));
	}
}

どうですか?奇数と偶数が見事に交互に出てきたと思います。

このrand関数、「線形合同法」と呼ばれる乱数ルーチンで、乱数の中でも最も簡単なものです。実はこのルーチンはunixの標準ライブラリを初めとするライブラリに搭載されている事が多いのです。(最近はもう少しマシな乱数積んでることもありますけど)

それを知らず、そのまま何も考えずにサイコロの出目にrand()を使うと・・・カルドセプトのバグが再現出来ますね。

回避方法としては自作の乱数処理を作るのが確実です。また、昔は何かボタンが押される度にrand()を実行して次に来る乱数をバラすとか、毎フレームrand()を実行して次に来る乱数をバラすとか、色々ワザがあったみたいです。

Seleneの乱数はどうなっているか詳しく調べてないのですが、まぁいざとなったらメルセンヌ・ツイスター(下記参照)を使います。

メルセンヌ・ツイスター乱数

参考ページ:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/mt.html

松本眞 ・西村拓士により1996年から1997年に渡って開発された疑似乱数生成アルゴリズム
長周期, 高次元均等分布を持つ
生成速度が若干rand()より早い
メモリ効率が良い
上記特徴を持つ乱数生成アルゴリズムです。
又2005年2月には64ビット用の生成コードも公開され始めました。
簡単な使い方ですが
{
   sgenrand(timeGetTime());

   乱数 = genrand() % 求めたい範囲
}
ほぼrand関数と同じ使い方で使うことができます。

static constを使おう


#define HOGEHOGE    (10)

static const int HOGEHOGE = 10;

どちらの場合でも HOGEHOGE = 10 なのですが、書き方としては下の方がベターです。
defineはプレコンパイルの時点で定数に置き換えられてしまい、デバッガで追えないのですが、static const の場合変数として存在し続けるので、デバッガで追うことが出来ます。

また、デバッガで追うことが出来るということは、無理矢理変数を書き換えることも出来るという事です。HOGEHOGEを20にして試してみたい…という時、defineだともう一度コンパイルし直さなくてはいけませんが、変数で定義してあれば変数の中身を書き換えるだけで済みます。依存関係のすごい定義だととても役立ちます。
最終更新:2009年03月07日 15:52