

Ｃ言語表記について



$Id: style.txt,v 1.1.1.3 2002/11/19 11:41:44 Yoshizawa1 Exp $



ＭＧＳ２でのＣのコーディングスタイルについて、基本的には

個人の自由に任せているが、新しくチームに加わる人達のために、

以下に私（植原）のコーディングスタイルを示しておく。



決して強制するわけではないが、参考にしてほしい。



以下、ランクを☆の数で表す。

☆☆☆☆☆　　できるだけ守ってほしい。（常識レベル）

☆☆☆　　　　守った方がいいと思う。

☆　　　　　　好き好き。



基本ポリシーは、

・手間を惜しんでバグを増やすな。

・３日前の自分は他人。

である。





■インデントは統一する　☆☆☆☆☆



必ずどのようなものであれ、インデントを行い、統一すること。

基本的にはＫ＆Ｒ系のインデントが（多数派という意味で）望ましい。

空白に関しても、自分が後で見るときのことを考えると、

各トークンごとにいれるのが望ましい。



int func( int arg, char *ptr )

{

    if( arg == 0 ){

        funcb( ptr );

    }

    return 0;

}



■注釈を書く、もしくはドキュメントを書く  ☆☆☆☆☆



人間は忘れる生き物である。個人差はあるが、短い場合は2～3時間、長い場合は

一ヵ月以上ソースを見ないで放置すると、自分の書いたものであっても何をする

処理なのかを思い出すのにいささかの努力が必要になる。



上にも書いたが、「３日前の自分は他人」なのである。



また、当然チームで作業している以上、担当の組換えや移植等で

他人にソースを見られることもありうる。



当然注釈はつけるべきであるが、出来れば「何をしているか」だけではなく

「どんな意図の処理をしているのか」といった概念的な部分までを文面に含める

ことが理想的である。



    if( <A> ) cnt++;  /* <A> なら cnt をインクリメント */



などというのは論外。C の構文を日本語に直しただけであって、

書くだけ無駄である。



    if( <A> ) cnt++;  /* <A> ならば次の要素の処理に移る */



程度には書くべきであろうと思われる。



ある程度複雑な概念をもった関数の先頭には、それが何をする関数なのかを

詳細に書いたほうが良い。今は自分以外がそのソースをいじらなくても、いつ

他の開発者への引き継ぎがおこるかわからないということを前提に書くべきであろう。

つまり、自分以外にそのソースを読む人にとって、理解の助けになるように書くわけである。



/*

 * buffer の画像データを転送

 */

int TransScreenShot( unsigned int * buffer, DG_DMATAG * packet)

{

    :

}

 

のような抽象的な注釈は避けるべきである。「どのような」画像ファイルを

「何処に」転送するのかがまるでわからない。



/*

 * buffer で指定された領域にあるスクリーンショットの生イメージを、

 * V-RAM のテクスチャページに転送するための DMA パケットを、

 * packet に作成する。

 * 正常終了ならば 0, 異常終了ならば非0 を返す。

 */

int TransScreenShot( unsigned int * buffer, DG_DMATAG * packet)

{

    :

}

 

程度には書くことが望ましい。



ソース中でも、流れに即してコメントをいれる。１行づついれるなどというのは

かえって読みにくくなるが、処理のブロック単位でいれるようにすると、後で

何をやっているか思い返しやすい。



特に、ちょっとでもトリッキーなことを行うような場合、こまめにコメントを

いれるようにしよう。

あとで、「あれ、これちゃんと動いているの？」とか自分で思わないように

するためである。



基本的にソース中のコメントは、「後で他人になる自分」のためにわかりやすく

書くように心がけよう。



ある程度規模が大きく汎用性の高い「モジュール」と呼べるものについては、

別にドキュメントをまとめることが望まれる。ドキュメントを書くときには、

最低限モジュールの運用概念(処理概要)、簡単なサンプル、詳細なリファレンスが

欲しいところである。





■どんな関数も、マクロ定義で置きかえられる可能性を考える　☆☆☆☆☆



たとえば、マスターではprintfを

#define printf( a, b... )

などとやって消してしまうことが行われたりするが、

このとき、

    if( ... ) printf( ... ), flag = 1;

とかやって、エラーを出してしまったソースがあった。

( if( ... ), flag = 1; と置換され、エラーになる )

面倒でも、if( ... ){ printf( ... ); flag = 1; }

とやること。（see ■{}は省略しない)



また、インクリメント等も危険である。

    func( p++ );

とかは、funcがマクロ定義になった場合バグを起こすことがある。

過去には、

    ASSERT( ( ptr = GetPtr() ) != NULL );

とやったソースもあった。この場合、マスターではASSERTマクロは

きれいさっぱり消えてしまうので、ptrを後で使うと、未初期化状態

で使用することになり、不定なアドレスを壊してしまっていた。



どんなパラメータにも、インクリメント、代入等は使わないようにした方が

賢明であろう。





■一行に複数の関数を記述しない　☆☆☆☆☆



一行に複数の関数を記述すると、その評価順は処理系依存であるため、

違う処理系に移植するときに問題が起こる。

たとえば、GetParam() なる関数が順番に値を返してくる場合、



value = ( GetParam() << 16 ) | ( GetParam() );



とか書くと、処理系によって、GetParam()の返してくる値の順番が

異なるので、colの値が変わってしまう。



まとまるからといって、安易にこのような呼び出しを行わず、



hi = GetParam();

low = GetParam();

value = ( hi << 16 ) | ( low );



のように書くべきである。

(変数は増えるが、最適化により、まったく速度差はないはずである)





■シフトを使うときはsignedとunsignedに注意する　☆☆☆☆☆



負の数の右シフト演算子は、signedとunsignedで挙動が異なるので注意。

Ｃの規格上は「実装依存」である。

ほとんどの処理系では、

( ( int )0x80000000 >> 24 ) は、0x0xFFFFF80

( ( unsigned int )0x80000000 >> 24 ) は、0x00000080

となる。





■signed char, unsigned charに気をつけよう。 ☆☆☆☆☆



charと書いた場合にunsigned charとみなす処理系もある。

signed charの場合には

{

	char c = 0xFF;

	if( c == 0xFF ){

		..

	}

}

のifの中は実行されない。（cがintに拡張されて-1になるから)

unsigned の場合は明示的にunsigned charで宣言するようにしよう。





■定数はdefineを使う　☆☆☆☆☆



これも、よくいわれることで、面倒くさいのはわかるが、

後で見返したときの数値の意味をわかりやすくするためにもかならず

意味のわかるマクロ名で記述しておくこと。





■Warningを無視しない　☆☆☆☆☆



コンパイラの出すWarningはきちんと見ておくことで、致命的なバグが防げる。

やばそうなのは、GCCの場合、



assignment from incompatible pointer type

    違う型のポインタにキャストした。

assignment makes integer from pointer without a cast

    ポインタからintにキャストなしで変換した。

implicit declaration of function `...'

    関数のプロトタイプがない。

control reaches end of non-void function

    返り値を返す関数なのに、返り値が設定されていない。

passing arg ? of `...' from incompatible pointer type

    関数の第?引数のポインタの型が違う。

type mismatch with previous implicit declaration

    関数宣言の前にその関数が使用されていて、しかも引数の型が違う。

`...' might be used uninitialized in this function

    ... が初期化されずに使用されている。（かもしれない）



などなど。

正しく書いていても出る場合はあるが、きちんとチェックしておくこと。

プロトタイプがない関数などは、気がついたら追加しておこう。

ＣＶＳで管理しているので、各個人がシステムなどの関数プロトタイプを

追加することも問題はない。むしろ気がついた人が率先して追加しよう。





■Warningを過信しない　☆☆☆☆☆



コンパイラの出すWarningはきちんと見ておくことで、致命的なバグが防げる。

ただ、Warningをとったからといって安心してはいけない。



たとえば、初期化されていない構造体が関数の入力に使われたとしても、

コンパイラはおかしいコードかどうか判別できない。



たとえば、



{

	VECTOR vec;



	CopyVector( &res, &vec );

}



とかである。

定義と参照の間が大きく開いていると、この手のバグは見つけにくい。



結果、不定な値が参照されることになる。

構造体は確実に初期化することを心がけるべきである。





■エラーチェックを省略しない　☆☆☆☆☆



ゲームプログラミングという意味では、値の整合性チェックなどは

処理速度の低下につながるため、省略されがちであるが、開発中は

こまめに入れたほうが結果的にデバッグ作業量の軽減につながる。

大抵のプロジェクトではマスター時とデバッグ時を変更できる

コンパイルオプションを設定しているはずなので、それを利用する。

MGS2では、

#ifdef DEBUG_MODE

#endif

や、

ASSERT( .. );

などでチェックをいれよう。

printfが使える環境では、printfで何がおかしいかを出力する。





■&,|,=,<<,>>等には()をつける　☆☆☆☆☆



条件文の優先順位で間違えやすいもの。

覚えるより、必ず（）を付ける習慣にしたほうが確実。





■変数名規約　☆☆☆☆



ハンガリアン記法まで行く必要はないと思うが、

名前から大体どういう変数かわかるのが理想。



localな変数は短めに、globalな変数は長めにして、

一目でわかるようにする。



大体、

ループ変数はi, j, k,

テンポラリはt,tmp,

ポインタはp

とかの名前を使うことが多い。



ただし、文字数をケチって可読性を損なうことはないように。

多少長くても後で読み返してすぐわかることを優先する。





■トップダウンとボトムアップ　☆☆☆☆



メインルーチンを上に、サブルーチンを下に書いていくトップダウンと

サブルーチンを上からメインルーチンを一番下に書いていくボトムアップの

両方の流儀があるが、ここではボトムアップをお勧めする。

理由は

・トップダウンではコンパイラはインライン展開してくれない。

・ボトムアップはサブルーチンのプロトタイプ宣言を別途する必要がない。

どちらもコンパイラによっては問題ないこともあるが。





■staticな関数　☆☆☆☆☆



staticな関数はそのファイル内でしか呼び出しできない。

名前の衝突を避けるため、下請け関数はstaticで宣言する。

ちなみに、一般的なCPU・コンパイラの場合、staticな関数を

呼び出す方がグローバルな関数を呼び出すより、ちょっとだけ速い。

(ブランチとジャンプの違い）





■グローバル変数へのアクセス　☆☆☆☆☆



グローバル変数に関しては、できるだけ少なくし、

アクセスに関しては直接参照／代入するのではなく、できるだけマクロや

関数を経由するのが望ましい。

（その変数がらみでバグが出た場合、トレースがしやすいように。

　また、変数の型が変更されることもありうる）





■if(!flag)などの多用の禁止　☆☆



最近のエディッタ等でフォントによっては、！などが隣の文字と重なって

非常に見にくいことがある。



if( !flag ){

}



等は便利ではあるが、

if( ! flag )や、if( flag == 0 )

の方が後で見やすい。





■if( flag == 1 )　☆



if( flag )と、if( flag == 1 )では、flagが0,1以外の時の挙動が違うことに注意。

関数の返り値を代入していたflagが、いつのまにかカウンターになっていて、

バグを引き起こした例があった。

また、( flag == 1 )と、( flag )では、実は１をロードする命令分

遅くなることがある（ＣＰＵ・コンパイラ依存だが）

上の ■if(!flag)などの多用の禁止 も合わせて、if( flag != 0 )とかにするのが

いいかもしれない。





■巨大な行数の関数をつくらない　☆☆☆☆☆



関数が巨大になると、

・よみづらい。

・最適化処理のために関数全体を解析するため、コンパイル時間が長くなる。

・コンパイラによってはコンパイラそのものがバグる。

など、加速度的に問題が発生してくる。

関数は、多くてもエディタ２、３ページにとどめ、それ以上になるなら、

関数として分けること。



とくに、コナミ社内で「step処理」といわれる、

switch( step ){

    case STEP_A:

        break;  

    case STEP_B:

        break;  

    .....

}

は、非常に大きな関数になることが多い。

これは、次のセクションで説明するように、分割した方が有利である。





■巨大なswitch-case は使わない　☆☆☆☆



step処理などで、switch-caseが巨大になることがよくある。

この場合、速度的に考察してみると、

コンパイラでは、caseの後の値の分散度合いによって、

if(){} else if(){} else if(){} .... else

か、テーブルジャンプかどちらかに変換している。

前者の場合は、いうまでもなく、比較・分岐が多く発生するため遅くなりやすく、

後者の場合は、テーブルジャンプとして実装する場合より

値の正当性チェック（どのcaseにもHITしない場合は何もしない）の分だけ遅くなる。

この場合は、関数へのポインタを使って、



typedef struct _work {

    ...

    void ( *func )( struct _work *work );

} Work;



static void funcA( Work *work );

static void funcB( Work *work );



static void funcA( Work *work )

{

    ...

    work->func = funcB;

}



static void funcB( Work *work )

{

    ...

    work->func = funcA;

}



void step( Work *work )

{

    ( *work->func )( work );

}



のように実装するほうがきれいである。

また、関数が増えすぎるようなら、各funcのなかで

さらにローカルなステップで管理するようにすれば、

見やすさと両立できる。その場合、関数名、マクロ名は

わかりやすくしておくこと。





■{}は省略しない　☆☆☆



よく、if()の後に一行になる場合、



if( .... )

    NewFunc();



のような書き方がある。これ自体は至極標準的で、

某ＮＥＣなどでは「こうしなければならない」とさえ

されているらしいのだが、私の経験上、



if( ... ){

    NewFunc();

}



と書く方が優れていると思う。

・統一がとれ、ソースの見た目でどこまでがif()内なのかわかりやすい。

・printf()などのトレーサーを入れたり、処理を追加するときに楽で、バグりにくい。

・何かの拍子（うっかりデリートを押しちゃった、リターンキーを押しちゃったなど）

  でインデントが崩れた場合もぱっとみでわかる。

というのが、「行数を節約できる」というメリットに勝るというのが理由。



また、同様に

    for( ;; )

       switch( status ){

        case HOGE:

            break;

       }

とかも、正しいが、非常にわかりにくいソースだと感じる。





■関数内ブロックをつかおう　☆☆☆☆



関数の中で{}でブロックをつくると、そのブロックのみでの変数を使用できる。

変数が多くなりすぎるのを防ぎ、スタックが効率よく利用できるのと、

コンパイラにとっても、変数の寿命を推定しやすくなるので、最適化の際に

有利に働くことがある。

どうしても大きくせざるを得ない関数の場合にとくに便利。

また、switch-caseの時も便利である。



void func( void )

{

    int var;

    

    ...



    if( ... ){

        int i;

        for( i = 0; i < ...; i++ ){

            ....

        }

    }

    {

        char buf[ 32 ];

        sprintf( buf, "....." );

		....

	}

	switch( var ){

		case STEP_1:

			{

				VECTOR v;

				....

			}

			break;

	}

}





■こまめに関数化　☆☆☆☆



カットアンドペーストで以前作った関数から処理をコピーしていくのは便利だが、

もしその中にバグがあった場合に、取り返しがつかないことになることがある。

コピーをするくらいなら、できるだけ関数として分割する方がのぞましい。





■処理系依存なコードは書かない	☆☆☆☆☆



Xboxへの移植の際、以下のようなコードが問題になった。



float f1, f2 = 1.1F;

int i;



i = ( int )f1 = f2;



これは、PS2のGCCでは、



i = ( int )( f1 = ( int )f2 );



として扱われ、

f1 = 1.0F;

i = 1;

となるらしいが、



XboxのVCでコンパイルすると、



i = ( *( int * )f1 = ( int )f2 );



として扱われ、f1 には、1.4013e-45なる不正な値が入ってしまっていた。



単純な代入参照では、問題ないが、このようにキャストが入ったときの解釈は

処理系依存となる。無茶なことはしないように。



なんにしても、処理系依存な動作を期待するコードは避けるべきである。

 ( ■一行に複数の関数を記述しない も参照のこと )



■まちがえやすいCの挙動について知識を身につけよう。	☆☆☆☆☆



たとえば、

(-5) % 4

はいくらになるだろうか。



正解は -1。

( -5 / 4 = -1 ... -1 )



ちなみに、( (-5) & 3 ) == 3。



たとえば、



unsigned int a;



if( a < -1 )...



みたいなコードがあった場合、このif文は常に真になる。

(-1がunsignedに拡張されて比較されるため)

