C/C++の最近のブログ記事
enumの列挙数って取得できないのかな。
/**
* ジョイスティック用キーコード
*/
enum E_JOY_CODE
{
JOY_UP = 0, ///< ボタン上 ※0始まりを明示
JOY_DOWN, ///< ボタン下
JOY_LEFT, ///< ボタン左
JOY_RIGHT, ///< ボタン右
JOY_BTN1, ///< ボタン1
JOY_BTN2, ///< ボタン2
JOY_BTN3, ///< ボタン3
JOY_BTN4, ///< ボタン4
JOY_BTN5, ///< ボタン5
JOY_BTN6, ///< ボタン6
JOY_CODE_END, ///< 最終メンバ(メンバ数取得用)
};
しょうがなく、最初の値を0って明示して、さらに最終列挙値を用意。これでJOY_CODE_ENDを見れば列挙数がわかるって細工したんだけど、他に上手いやり方ないか。
こんな構造体より、
/**
* ジョイスティックの入力状態
*/
struct ST_JOY_STATE
{
unsigned char ucJoyUp; ///< 上キー
unsigned char ucJoyDown; ///< 下キー
unsigned char ucJoyLeft; ///< 左キー
unsigned char ucJoyRight; ///< 右キー
unsigned char aryucJoyBtn[6]; ///< 数字キー
};
ST_JOY_STATE _stJoyState; ///< ジョイスティックの入力状態
こんな列挙型と配列使ったほうが、
/**
* ジョイスティック用キーコード
*/
enum E_JOY_CODE
{
JOY_UP, ///< ボタン上
JOY_DOWN, ///< ボタン下
JOY_LEFT, ///< ボタン左
JOY_RIGHT, ///< ボタン右
JOY_BTN1, ///< ボタン1
JOY_BTN2, ///< ボタン2
JOY_BTN3, ///< ボタン3
JOY_BTN4, ///< ボタン4
JOY_BTN5, ///< ボタン5
JOY_BTN6, ///< ボタン6
JOY_CODE_END, ///< 最終メンバ(メンバ数取得用)
};
unsigned char _aryucJoyState[JOY_CODE_END]; ///< ジョイスティックの入力状態
管理しやすいような気がした。
上のE_JOY_CODE列挙型があれば、ボタンの情報を関連付けやすいし、そんで構造体より配列の方がコードがシンプルになる。
※「///<」はDoxygen用のコメント
Part3 JavaScriptに学ぶ「言語の拡張性」 - ITpro
関数はローワーキャメルケース(先頭が小文字のキャメルケース)になっています。これは,クラス・ベースのオブジェクト指向言語では関数(メソッド)の命名規則がローワーキャメルケースであることに準じています。
オブジェクト指向言語だと、関数名はローワーキャメルケースにするのが一般的なのか。アッパーキャメルケースにしてたわ。直さなきゃ。ひたすら一括置換だ。
if文、for文とかにつける"{"についての話。
C、C++のコーディングだと、改行してから"{"を付ける。スクリプト言語とかWeb系の言語だと、右に"{"を付ける。もちろん規約があればそれに従う。無ければ、一般的な方に合わせる。
右"{"のとき、分岐処理のコメント前に改行するかしないかですごく悩む。
改行"{"だと、
// Aのとき
if (a == x)
{
FuncA();
FuncB();
}
// Bのとき
else if (b == x)
{
FuncC();
FuncD();
}
// その他
else
{
}
特に問題なし。
これが右"{"になると、
// Aのとき
if (a == x) {
FuncA();
FuncB();
// Bのとき ←この行の密集してる感が嫌だ
} else if (b == x) {
FuncC();
FuncD();
// その他
} else {
}
こんな風にごちゃごちゃする。じゃあ、コメント前に改行入れようと。
// Aのとき
if (a == x) {
FuncA();
FuncB();// Bのとき
} else if (b == x) {
FuncC();
FuncD();// その他
} else {
}
でも、コメント前改行ってことにすると、こんなときに↓
ってことになる。だから、密集してる感が漂ってたら、改行しましょうってことにした。// Aのとき
if (x == a) {
if (y == i) {
FuncA();
} else if (y == j) {
FuncB();
}
←この不自然な改行が気持ち悪い
// その他
} else {
}
でも、どういう基準で改行入れてるんだってことになって、、、あー、めんどくせ。。。
自分めんどくせ。
ゲーム処理クラスがDirectX処理クラスを直接使ってるけど、ゲーム処理も、描画エンジンとかに依存せずに独立したプログラムにしたい。ということで、一つクラスをラップすることにした。[]はクラス。
[DirectX Graphics処理]←――[描画] ←┐
[DirectX Audio処理] ←――[サウンド再生] ←+―[ゲーム処理]
[DirectInput処理] ←┬―[入力制御] ←┘
[Keyboard入力] ←┘
ゲーム処理クラスはDirectXなんて知らない。だから、描画クラスだけいじれば、ゲームプログラムを一切変更せずに描画エンジンが切り替えられる。
共通処理クラスCCommonっていうクラスを作ってて、その中でエラーログ出力関数みたいなどこでも使うような関数を用意して、いろんなとこで使ってたんだけど、DirectX処理クラスでこの共通処理クラスを使うのは、ちょっと変な気がしてきた。
というのも、DirectX処理クラスは、完全にゲームとは別に独立したものっていう考えで、本来はDLLとして存在するべきものにしてある(DLLにすると逆にめんどいことがあるんで、今は敢えてDLLにしてない)。だから、今作ってるゲームに限らず、このDirectX処理クラスは汎用的に使えるようにしてある。
本来独立して存在するものが、ゲーム処理クラスと同じ共通処理クラスを使用してるのは変。だから、DirectX処理クラスは、共通処理クラスを使わない方がいいな。
そもそも共通処理クラスっていうのが抽象的すぎた。ゲーム処理用の共通クラスとでもしておくか。
共通処理クラスに、デバッグトレースの関数を作った。
/**
* デバッグトレース
*
* @param[in] a_pszFormat 書式
*/
void CCommon::Trace(const char* a_pszFormat, ...)
{
#ifdef _DEBUG
va_list pcParam; // 引数リスト
char szTrace[512]; // 出力文字列// 可変引数の取得開始
va_start(pcParam, a_pszFormat);
// 出力文字列の作成
_vsnprintf(szTrace, sizeof(szTrace),
a_pszFormat, pcParam);
// 可変引数の取得終了
va_end(pcParam);// デバッグトレース
OutputDebugString(szTrace);
#endif
}
この関数はエラー発生時とかに使う。
// エラー発生時
if (! bRet)
{
CCommon::Trace("Errror: %s %s\n",
__FUNCTION__, pszErrorMessage);
}
上のフォーマットだと、例えば、
「Errror: CDI::Init DirectInput8オブジェクトの作成に失敗」
って文字列が出力されるんだけど、いろんなとこでこのトレースは使われるわけなんで、出力フォーマットは必ず統一されるようにした方がいい。
それで、直接トレース関数を使うわけじゃなくて、エラー発生用に使うトレース関数を別に作っといた。
/**
* エラー用デバッグトレース
*
* @param[in] a_pszFunction 関数名
* @param[in] a_pszError エラーメッセージ
*/
void CCommon::TraceOfError(char* a_pszFunction, char* a_pszError)
{
CCommon::Trace("Errror: %s %s\n",
__FUNCTION__, pszErrorMessage);
}
とりあえず、エラー発生した関数名とエラーメッセージだけ出力すればいいかな。ということで、エラー発生時に書く処理はこれ↓。
// エラー発生時
if (! bRet)
{
CCommon::TraceOfError(__FUNCTION__, pszErrorMessage);
}
ゲームプログラムで使うデバッグトレース、ログ出力、エラーメッセージ表示について。それぞれどういうタイミングで使用するべきか、どういう位置づけで使用するべきか、決めてみた。
[デバッグトレース]
- ファイル読み込みや、シーンの切り替えなど、重要処理や全体の処理フローがわかるようにトレースする
- エラー発生時に、そのときの関数の名前や行数などをトレースする
[ログ出力]
- エラー発生時に、そのときの関数の名前や行数などを、エラーログとして出力する
[エラーメッセージ表示]
- エラー発生時に、そのことがプレイヤーにわかるよう、メッセージダイアログにて、エラーメッセージを出力する
- 基本的には、ゲーム起動時に出力する
例)「DirectXの初期化に失敗しました」
「画像ファイルの読み込みに失敗しました」
重要度でいくと、
エラーメッセージ表示 > ログ出力 > デバッグトレース
っていう順番かな。
Cで時刻を表すのに、time_t型を使ってるけど、その型が2038年の1月位までしか対応してない。調べてみたら、2038年問題ってのがあった。
2038年問題 - Wikipedia
2038年1月19日3時14分7秒を越えると、この値がオーバーフローし、負と扱われるため、誤作動する可能性が高い。
time_t型のサイズを増やしても、まだ問題があるらしい。
2038年問題と同じように考えると、292277026596年に桁あふれが起こる可能性があり、これを「292277026596年問題」と呼ぶこともある。
292277026596年。ニクニ、ニナナオ、ニムゴクムね。勉強になるぜ。
ファイルから読み込んだバイナリデータを扱うプログラムを改造してんだけど、読み込むファイルのデータがビッグエンディアンになってて、動かす環境はx86系でリトルエンディアンなんで。
1バイトだけのデータとか文字列は別にエンディアンを考慮する必要なし。1バイトずつ見る分にはひっくりかえりようがないから。2バイト以上の整数値は変換しないといけない。
memcpy(&shNum, pFileData, sizeof(short));
memcpy(&lNum, pFileData, sizeof(long));
とかやったら、shNum、lNumは変換必要。そもそも、memcpy関数使わないで、エンディアン変換込みのメモリコピー関数を独自で作ればいいけど。
嫌だったのは、バイナリデータが構造体を表してて、それを取得するところ。構造体は、long型(4バイト)とかのメンバをめちゃくちゃ持ってた。
struct ST_TEST
{
long lDataA;
long lDataB;
long lDataC;
…
…果てなく続く(嘘)…
}
この構造体をそのまま
memcpy(&stTestData, pFileData, sizeof(ST_TEST));
ってしたら、↓こんなバイナリデータが、
00 00 00 01 00 00 00 0A 00 00 00 02 …
それぞれ4バイト区切りでひっくり返った値になって解釈されちゃう。↓こんな風に。
lDataA:0x01000000 ※本当は0x00000001というデータで欲しい
lDataB:0x0A000000
lDataC:0x02000000
…
というわけで、せっかく構造体として存在してるのに、わざわざメンバ一個ずつ見てエンディアン変換かけることに。めんどい。
リトルエンディアンって、よくいう「バッドノウハウ」ってやつになるのかな。
関数内で、do~while (false)を使うのが好き。
使う人は当たり前のように使ってるけど、この書き方が嫌いな人も結構いるっぽい。
メリットは、
- if文の多用や、それによるインデントの数を防げる
- 終了時の処理が分散しない(途中returnを無くせる)
ってとこだと思う。
実際↓こんな感じになる。
/**
* なんかの初期化関数
*
* @param[in] a_nIn 入力値
*
* @return 処理結果
* @retval true 成功
* @retval false 失敗
*/
bool Init(int a_nIn)
{
bool bRet = true; // 戻り値
char* pszError = NULL; // エラーメッセージ
do
{
// パラメータチェック
if (a_nIn < 0)
{
pszError = "パラメータエラー";
bRet = false;
break;
}
// オブジェクトの生成
if (! CreateObject())
{
pszError = "オブジェクト生成エラー";
bRet = false;
break;
}
// オブジェクト2の生成
if (! CreateObject2())
{
pszError = "オブジェクト2生成エラー";
bRet = false;
break;
}
}
while (false);
// エラーメッセージ表示
if (! bRet)
{
OutputErrorMessage(pszError);
}
// ログ出力とか?必要なら
OutputLog(__FUNCTION__);
return bRet;
}
途中returnあると、終了時の処理が分散しちゃって見にくいし、管理しにくい。終了処理は一箇所にまとまってる方がいいかなと。
ちなみに、while文使って、
while (true)
{
// パラメータチェック
if (a_nIn < 0)
{
pszError = "パラメータエラー";
bRet = false;
break;
}
…(略)…
break;
}
でもいいかと思ったけど、会社の人と話したときに、
「breakは忘れるから怖い」
って言ってた。while(true)でbreak忘れて無限ループになるのが怖いと。
do~while文だったら、最後のwhile忘れたらコンパイルがそもそもできないし、その最後のwhileにfalseじゃなくて敢えてtrueを指定するって間違いは無いと思う。
でも、while文のときは、while(true)の中で最後にbreak忘れても、コンパイル通っちゃう。そのbreakだけ忘れるとかだったらやっちゃいそう。
あ、goto文は存在しないことにしときます。
コンストラクタでメンバ変数を初期化する際には、代入じゃなくて、初期化リストを使う。
/**
* CSampleクラス
*
* @date 2008/04/30
*/
class CSample
{
int _nTest;
int* _pnTest;
…
}
こんなメンバ変数持ったクラスだったら、初期化リスト使った初期化はこうなる。
/**
* コンストラクタ
*/
CSample::CSample():
_nTest(0),
_pnTest(NULL)
{
}
で、メンバ変数が配列とか構造体のときは、どうすんのかと思って調べたけど、見つからなかった。試しに次のメンバ変数を初期化リストに入れてデバッグしてみた。
/**
* CSampleクラス
*
* @date 2008/04/30
*/
class CSample
{
int _arynTest[100]; ←配列
ST_TEST _stTest; ←構造体
…
}
初期値を引数に入れようとするとエラーになったから、空にしてビルドした。
/**
* コンストラクタ
*/
CSample::CSample():
_arynTest(),
_stTest()
{
}
で、このとき構造体"_stTest"は0埋めされてたから、とりあえず構造体は0で初期化されるっぽい。ただ配列"_arynTest"は0埋めとかされてなかった。この方法だと配列は初期化できないみたい。
配列のメンバ変数の初期化は、普通に{}内で、memsetとか使うしかないか。
C++でも自動でドキュメント生成してくれるツールあるに決まってるよなってことで、調べたら、Doxygenっていのが一般的っぽいんで、これを使うことにした。ヘッダコメントとかもDoxygen対応の書式にした。
インストールしたら、DoxywizardってGUIのツールができるから、これ使ってちょっと設定変えて、[Start]ボタン押すだけでドキュメントが生成できた。
コメントの書き方は、QtスタイルとJavaDocスタイルがあるけど、自分はJavaDocスタイルに慣れてるからこれを使うことにした。
設定をいじったとこは、
- [OUTPUT_LANGUAGE]をJapaneseへ
- [INPUT_ENCODING]をShift_JISへ
- privateメンバを表示するように、[EXTRACT_PRIVATE]を有効へ
- コメント1行目の概要を表示するように、[JAVADOC_AUTOBRIEF]を有効へ
- ファイル一覧でフルパスが表示されないように、[FULL_PATH_NAMES]を無効へ
ってとこかな。
で、ヘッダコメントは↓こんな感じにした。
- ファイルヘッダ
/**
* @file Di.h
*
* @brief DirectInput処理
*
* @author 1122
* @date 2008/01/18
*/
- クラスヘッダ
/**
* DirectInput処理
*
* @author 1122
* @date 2008/01/18
*/
- 関数ヘッダ
/**
* DirectInput初期化
*
* @param[in] a_hWnd ウィンドウハンドル
* @param[in] a_hInstance インスタンスハンドル
*
* @return 処理結果
* @retval true 成功
* @retval false 失敗
*/
前にC++で作ってたゲームが久々気になり、ソースコードを眺めてた。そしたらそのコードにイライラしてきたので、リファクタリングを始めた。
学生の頃に作ったコードだから、もちろんコーディング規約を作ってるわけはなく、暗黙の了解という感じでコーディングの仕方をそろえてた。それで何となくは規則性のあるコードが書かれてたけど、ただ規則的なだけで、わかりにくい記述がいくつかあった。そんで、まずは規約を作ってみた。
関数のヘッダコメントも規約として決めた。
↓例えばこんなん。
//----------------------------------------------------------
// Name : Init
// Desc : 初期化
//
// Arg : [in] int a_nIn : 入力値
// [out] int* a_pnOut : 出力値
//
// Return : int nRet : 戻り値
//----------------------------------------------------------
引数のとこを"Arg"にするか"Param"にするかというどーでもいいことで悩んだ。自分としては"Param"って書いた方がパッと見わかりやすい。ただね、今回作った規約では、変数名はハンガリアン記法ってことにしてある。ハンガリアン記法だと、引数のプレフィクスは"a_"が一般的。で、この"a_"っつーのは、Argumentの頭文字。そうすると、引数の説明は"Arg"にしないと統一感ねーよな…、となり、散々悩んで、"Arg"に決定。こういうとこでも優柔不断全開。
JavaDocとかphpDocumentorみたいに、C++ Docとかいうのがあればなぁ。。。
ハンガリアンなんだけど、これは元々嫌いだったんだけど、仕事で使ってみたら、コード理解しやすいってことがわかり、なるべく使うようにしてる。






