ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail
スタック上に置かれるオート変数は、初期化しないとどんな値が入っているのか知れたものではありません。 コードとしては多少冗長になるかもしれませんが、オート変数を定義した時点で有意な値に初期化する癖をつけておくと、初期化忘れによるバグの数がほぼ皆無になります。
もちろんものごとには例外がありますが、特に初心者の場合、変数を定義したら初期化する、という原則を知っておくべきです。 さもなければ、いつか高い勉強代を払う羽目になるでしょう。
もちろん、C++ならすべてのメンバをきっちりコンストラクタで初期化するのは言うまでもありません。
オーバーヘッドといっても、これだけCPUやメモリが高速化している昨今、 ほんの数サイクルのCPU節約よりも、バグフリーなコードを書くほうがよほど大事です。 また、条件がそろえばコンパイラのオプティマイズで冗長なコードが消えることもあります。 不要な初期化を行わずにすむようなコードを書くことも大事でしょう。
なお、VC++6.0のデバッグモードでは、未初期化オート変数は0xccで埋め尽くされるので、未初期化データのバグを検出するのが以前よりは容易になっています。
とは言っても、OSのカーネルのように、何万回も実行され、実行時間にシビアでCPUクロックの節約が全世界的に影響を及ぼす場合などはまた話が違ってきます。 でも、あなたの書いているコードは本当にそれほどシビアですか?
C++では、関数内のほぼどこにでも、変数の定義を書くことが出来ます。変数のスコープは宣言されてから後で有効なので、必要になった時点で変数を定義すると、不用意な変数の書き換えを防ぐことが出来ます。
7つの事柄を認識できるといわれるカラスと、人間のどちらが賢いのか分かりませんが、人間の頭が一度に覚えておけることはそう多くありません。 複雑な関数になるとそれに応じて変数の数も増えていくでしょうが、なるべく内側のブロックで変数を定義する癖をつけましょう。 関数全体を通じて生きている変数の数は激減します。 すなわち、一度に考えなければならない相手の数が減るわけです。 ものごとはなるべくシンプルに。コードが十分にシンプルなら、バグの紛れ込む可能性もそれだけ減ります。 そのためには、相手にする要素の数を減らすこと。これ大事です。 変数を定義したら初期化も忘れずにね。
ただし、注意点がひとつ。 ブロックの外ですでに宣言されている変数と同じ名前の変数を局所的に定義して使うことは避けましょう。 文法的には問題がなくても、のちのち大きな落とし穴になること必至です。
最近のコンパイラのオプティマイズはどんどん高度になってきて、次のような冗長なコードもかなりうまい具合にコンパイルしてくれます。
if (condition1) { FooBar(arg1, EnumVal1, arg3); …1 } else { FooBar(arg1, EnumVal2, arg3); …2 } ...
しかし、コンパイラにとってオプティマイズの腕の振るいどころであったとしても、コーディングする人間のほうはそうはいきません。コードを書いてしばらくして、 バグが見つかったとしましょう。 デバッグした結果、 arg3を左へシフトすればOK、とおもむろにコードを書き換えるのは結構ですが、そのとき1も2も忘れずにきちんと修正できるでしょうか。もし1と2が離れた場所にあったとしたらどうでしょう。
上記の例では、1番目と3番目の引数は同じで、conditionによって2番目の引数のみを変えたいわけなので、次のようにコーディングすればバグの入り込む余地が少なくなります。
Arg2 arg2 = EnumInvalid; if (condition1) { arg2 = EnumVal1; } else { arg2 = EnumVal2; } ... ASSERT(arg2 != EnumInvalid); FooBar(arg1, arg2, arg3);
要は、似たようなコードを何度も記述しない、ということです。コーディング時にも楽が出来ますが、その後のデバッグでも大いに楽が出来ます。プログラムのコーディングでは、日常生活とは違って、苦労して結果が良く なることはまれです。大いに楽をしてください。それがバグのないコードにつながります。
キャストはどうしても使わざるを得ない場合を除き、極力使用を避けるべきです。ANSI-CやC++コンパイラにせっかく備わっている、型の不一致によるエラー検出が、キャストを使うと何の役にも立たなくなります。
キャストを使わなければならない場合は、設計におかしな点があるのかも知れません。キャストの使用を余儀なくされたら、設計を疑ってみるぐらいのつもりでいても良いのかもしれません。
…とは言え、Win32やMFCでプログラミングしている場合、キャストがどうしても避けられないケースには良く遭遇します。 どうしてもキャストを使う場合には、C++のキャスト用オペレータや、MFCのSTATIC_DOWNCAST()、DYNAMIC_DOWNCAST()が使えないか、検討してみましょう。
3項演算子を使うと、コンパクトな記述が出来る場合もありますが、実際に生成されるマシン語は、if文と比べて効率が良くなるわけではありません。 マクロにはブロックを記述できないこともありますから、3項演算子が必須なこともあります(でも可能ならインライン関数を使いましょう)。 それ以外の場合なら、使ったほうがきれいに記述できる場合を除き、3項演算子を使うのは避ける方が無難です。
C/C++では、式の値が0以外なら、真になります。TRUEはたいてい1に定義されているので、TRUEと式の値を比較しても真にならない場合があります。
if (expression == TRUE) { ... }
このコードは冗長であるばかりか、バグの温床になります。
if (expression) { ... }
これで一向に構わないのです。どうしてもTRUE、FALSEと比較したいのなら、
if (expression != FALSE) { ... }
と書くべきです。
また、C++ではbool型とtrue、falseが使えます。bool型の取れる値はtrueかfalseのどちらかなので、少しは安心です。が、bool型の変数に論理値をいったん代入するか、キャストしない限りtrueまたはfalseと比較するのは これまたバグの温床になります。
冗長な{ }のないコードは一見かっこいいのですが、果たしてかっこよさに見合う実益があるのでしょうか。 コーディング時というより、デバッグあるいはメンテナンスなど後々のことを考えると、{ }の省略はしないほうが無難です。
中程度以上の規模のプログラムでは、バグの検出を容易にするための冗長なコードを埋め込みます。MFCを使っていれば、ASSERT(), VERIFY()をはじめとする組み込みのデバッグ支援コードを容易に書くことが出来ます。
バグを作ったことのないプログラマは皆無でしょう。もしそういう人がいるなら、それはたいしたプログラムを作ったことがないか、バグに気が付いていないだけでしょう(笑)。また、小さなプログラムを除き、完全にバグ フリーなプログラムというのも滅多にありません。たいていの場合、バグが顕在化していないか、「仕様」という伝家の宝刀でうやむやにされてしまっただけでありませう。
バグのないプログラムを書くのが至難の業である以上、バグとうまく付き合う方策が重要になります。コード中にデバッグ用コードを埋め込むのは、早期のバグ発見に役立つと同時に、プロジェクトが進んだ後でもリグレッ ションの検出に大いに役立ちます。何気なしに埋めたデバッグ用コードにすくわれた経験は数知れません。
バグの原因の一つには、プログラマの勝手な「思い込み」があげられます。思い込みをコーディング直後のテスト段階でキャッチできれば、その後のデバッグは非常に楽になります。 アサート(ASSERT)マクロには、デバッグ時にのみ評価される式を記述し、「ありえない」状態が実際に起きないことを確認します。リリース用のビルドではASSERT()マクロは消えてなくなってしまうので、実行時のオーバー ヘッドはありません。 例えば、リスト中に必ず検索対象の要素が含まれているとき、
struct List { List* next; MyType type; }; List* p; for (p = some_list; p; p = p->next) { if (p->type == sample) { break; } } ASSERT(p != NULL);
と記述しておけば、予想外の事態を確実にキャッチできます。
非MFCなプログラムでは、assert.hをインクルードすることでassert()マクロが使用できます。リリースビルドでは、NDEBUGを定義することでデバッグ用コードは消えてなくなります。
なんでもかんでもアサートにすれば良いというわけではありません。アサートはリリースビルドで消えてなくなるものなので、副作用のある式を評価するのは原則として避けるべきです。
ASSERT(i++ >= 0);
このようなアサートは非常に危険です。同じ理由で、ASSERT()中の関数呼び出しも、原則として避けるべきです。 呼び出す関数がリスト構造のバリデーションなど副作用がない、デバッグ専用のコードである場合に限り、関数を呼び出してもOKでしょう。ASSERT()中でC標準関数(CRT)を呼び出すなど、もっての他です。どこで副作用が発 生しているか分かったものではありません。現バージョンのCRTでは大丈夫に見えても、将来の版では実装が変わる可能性があります。
どうしても、リリースビルドでも実行するがデバッグ時にのみエラー状態の検出を行いたい、という場合には、VERIFY()マクロを使います。
アサートを使い倒せるようになってくると、今度は逆にアサートの誤使用に気をつけなければなりません。
例えば、次のようなコードは、一見アサートを使っているのでよさげに見えます。
char* p = (char*)malloc(sizeof *p * n_elements); ASSERT(p);
このアサートでチェックしているのは、実際にはランタイムエラーに他なりません。mallocが絶対に成功するという保証はありませんから、上記のコードで本当に必要なのは、メモリのアロケートに失敗した場合の後処理な のです。
使い捨てプログラムでもない限り、エラー処理を省略するのはやめましょう。
コンピュータの実行中には、どんなことでも起こりえます。 メモリがアロケートできない・ファイルに書き込めない・ウィンドウが作れないなどのリソース不足に始まり、あるべきファイルがそこにないとか、サーバが落ちているとか、来るべきでないタイミングで通知が届いたとか …。 いくらでも考えられます。 うまくいく道筋はひとつしかありませんが、エラーを起こす要因は数限りなくあるのです。 きちんと商業用に書かれたプログラムでは、コードの半分以上がエラーに対する処理であると言っても過言ではありません。
メモリやリソースのリークはその場ではすぐに分かりくいバグですが、いつか必ず痛い目にあわせてくれます。 デバッグのときではなく、コーディングするときにリークを起こさないように十分気をつけるべきです。 リークを見つけるのではなく、発生させないようにするのが一番、ということです。
たとえば、関数からエラーリターンするときにメモリの解放し忘れが生じるかもしれません。 デストラクタの使える C++ では起きにくくなっていますが、C では自分が気をつけるしかありません。 個人的には、リターン前のクリーンアップを一箇所にまとめるために goto すらばんばん使っても OK と思っています。
バッファのオーバーランを避けるため、入力や結果のサイズをチェックするよう心がけましょう。
デバッグビルドでは番兵を置いてオーバーランをチェックするのもありです。 とにかく自分を過信しないようにしましょう(自戒を込めて)。
C++に慣れる頃に、C++の機能を全部使わないと気がすまない、という病気にかかってしまうことがあります。 演算子オーバーライドの多用もその一つ。 もちろん、演算子オーバーライドを使って、きれいにコードが記述でき、見た目にもすっきりすることもありますが、何事もほどほどに。
トリッキーなコードはかっこ良さげなのですが、必要がなければ避けておきましょう。 他人に分かりにくいコードは、3ヵ月後の自分にも分かりにくいものです(笑)。 何がトリッキーなコードであるかの判断は、チームのレベルにもよるので難しいかもしれませんが…。
トリッキーなコードが全部駄目というつもりもありませんが、まぁコメントくらいはつけておきましょう。
コメントが不要なコードが一番です。 やっていることが一目瞭然であったり、適切な関数・変数の命名、必要のないときに変に凝ったことをしないなど、 ストレートなコーディングを心がけていれば、コメントがなくても分かりやすいコードが書けることも多々あります。 長いことコードを書いていれば、その辺のセンスは身についてきます。 良いコードをたくさん読んで、分かりやすいコードを目指しましょう。
が、パフォーマンスを出すためや、トリッキーなコーディングで全体のコード量が減らせるなど、 ストレートなコードをわざと書かない場合もあります。 また、当面相手にしているコードの外に、暗黙の条件が仮定されている場合もあります。 上記のASSERT()をぶち込んで、仮定した条件を明文化するのがお約束ですが、 このような場合には、読み手に情報を与えるコメントが有効でしょう。 もちろん、「読み手」には3ヵ月後の自分も含まれます(笑)。
ただし、後々コードを変更したとき、対応するコメントも適切にアップデートする必要があります。 これを怠ってしまうと、コメントは信頼できないものになり、その意味は全くなくなります。 コメントもコードの一部として認識し、コード本体と矛盾のないようにする習慣をつけておくべきでしょう。
また、各々のソースコードの冒頭に、おおざっぱに何をするコードが入っているのか記載しておくと、 これまた後で助かります。 重要な変更に関して、変更ログを残しておく場合もあります。 一般には、適切なソースコード管理ツールを用いるべきですが。
逆に、コメントの付け過ぎもあまりいただけません。 また、コードからストレートに読み取れる情報は、わざわざコメントに書く必要はありません。
使い捨てのプログラムでない限り、ソースコードは管理ツールを使ってバージョン管理しておきましょう。 理由は自明なので省略。 RCS, CVS, SCCSなどが定番。 Visual Source Safeやらの市販品もありますが、まぁお好みで。
…とは言っても、目の前で相手にしている環境以外でもきちんと動くプログラムを書くのは、結構難しいものです。 GUI周りはまぁ仕方ない側面がありますが、それ以外の汎用ロジックは、CPUや処理系に依存しないように意識しながらコーディングしてみましょう。 実際のところは、エンディアンに思わず依存しているところなんかがあったりして、移植するまで気が付かないことも多いのですが…。
なお、私の経験から言うと、エンディアンの問題はリトルエンディアンからビッグエンディアンに移植するときに多く発生します。 逆のケースはあまり記憶にありません。 それだけリトルエンディアンが便利というかナチュラルというか、そういうことなんだろうと個人的には思っていますが。 そこにCPUがある限り、そんなことは言ってられないのです(笑)。
ビットフィールドの割付も、処理系によって様々です。 unionで使ってunsigned charをビットに分割する小手先テクニックを使う場合には、記憶にとどめておきましょう。 実際の対処法としては、#ifdef かなんかで定義を変えることぐらいしか思いつきませんが…。
なにも、setuid やら ACL やらをきちんといじれ、と言っているわけではなく、意外なところからやってくるアタックを意識してプログラミングしましょう、という基本的なことです。 ファイルやレジストリからの入力、とりわけ外部(コンピュータの外)から届く可能性のある入力は、悪意のあるものかもしれません。 たとえば、やたらに長い URL やファイル名を送りつけてくるなど、バッファオーバーランを狙ってコンピュータを乗っ取ろうとするアタックの可能性があるわけです。
というわけで、 入力を信頼せず、文字列なら常に末尾を確かめよう、strcat などバッファ長を取らない文字列関数は使わず、strncpy などのより安全なライブラリ関数を使う、あるいはCString や string などのラッパを 使う、などとセキュリティに気を使う必要があるわけです(プログラムの種類にもよりますが)。
ここのセキュア・プログラミング講座は比較的まとまっていると思います。 一度読んでみると、得るものがあるかも知れません。
エンバグの原因です。
余計な最適化はしなくても良いでしょう。
たいていの場合、最適化したい場所は実行時間にクリティカルではありません。
本当に速度・サイズの向上を考えて最適化しているのですか、それとも最適化したいから最適化しているのではないですか(笑)?
本当にプログラムの実行速度を向上する必要があるのなら、きちんとプロファイラを使いプログラムのどこで時間がかかっているのかきちんと調べましょう。
余談ですが、往々にしてボトルネックになっている場所は最適化しにくいことが多いようです。
あぁ、また新たなマーフィの法則か…。
PC で動かすコードでは、本当に時間にクリティカルなところは速度向上を目指した最適化を、それ以外ではサイズの縮小を目指した最適化が一般に有利です。 というのも、今日では CPU の処理速度を少々稼ぐよりも、システム全体としてワーキングセットを小さくする方が大きな目で見て有利だからです。
なにしろメモリが足りなくなったりページアウトするときの相手は CPU からみれば超遅いハードディスクです。
ページインなどディスクからの入出力を待っている間、そのスレッドは休眠させられてしまいます。
メモリが相手でもこの原則はあてはまります。
CPU のキャッシュだけではすまないメモリアクセスは、実際にメモリからデータが転送されるまで CPU がブロックされます。
いくらキャッシュのヒット率が上がったといえ、少しでも多くのメモリをキャッシュできるほうがメタな実行速度には有利です。
というわけで、特に理由がない場合はサイズを小さくする最適化を心がけましょう。
データ、コードのどちらとも。
コンパイラに指示する最適化オプションについては、全体に対してはサイズ優先にします。 必要な場所でのみ #pragma を使って最適化オプションを変える、というのがお勧めなわけです。
今どきの PC はキャッシュ内にデータがないとかなり待たされるので、メモリアクセスはなるべく局在化しましょう。
たとえば、多次元配列なら内側からループをまわす、などは誰でも知っていることです。
せこい話だと、グローバル変数をローカル変数にコピーしてからループ中で使うと、実測で速度が向上することがあります。
ローカル変数はスタック内にあり、esp または ebp からの相対アドレッシングになります。
現在の関数で使っているスタックはまず間違いなくワーキングセットの中にあるでしょうしかなりの確率でキャッシュに存在すると期待していいでしょうから、メモリアクセスに待たされる可能性のあるグローバル変数より
速くなることがあるのでしょう。
上記のはあまりいい例ではないかもしれませんが、巨大な配列にアクセスするときなどには、一度に見るエリアを近所に置くかどうかではっきり速度が変わってきます。
いまどきの CPU はページ単位でメモリを管理しています。 頻繁に実行される関連性の高い関数を同じセクション(セグメント)にまとめておくと、同じまたは隣接したページに割り当てられることが期待できるので(詳しくは link -dump -all か map ファイルでも見てみましょう)、 多少実行効率がよくなるかも知れません。
小手先で行うよりも、アルゴリズムの見直しなどで最適化を行うほうがよい結果を生むことが多いでしょう。 小手先の最適化は最後の最後の手段、絞りかすからさらに数滴のレモンをスクイーズするような心構えでいいのではないでしょうか (とはいえ、プログラムによっては小手先テクニックが大いに効くことも多々あるのですが…)。
マルチプロセス・マルチスレッドでも実行単位の中では逐次実行に変わりありませんが、他との協調を考えずにプログラミングしていると、起きたり起きなかったりする嫌なバグが簡単に生じます。 対称型マルチ CPU をサポートした OS が主流に躍り出て、さらにはハイパースレッディングなどの技術によって本当のマルチスレッドがより一般的なものとなりつつあります。 もしマルチスレッド対応の正しいコーディングがなされていなかった場合、本当のマルチ化によりこれまで以上に問題が顕在化することが予想されます(シングル CPU での擬似マルチスレッドでももちろん問題になっていた のですが起きる頻度は相対的に低かった)。
とあるサウンドカードのドライバは、シングル CPU のみ対応をうたっていました(現在の状況は知りませんが)。 想像するに、ドライバ内部できちんと排他処理をせず適当にごまかしていたのかもしれません。 たとえば割り込みを禁止するとか、IRQL をあげておくとかで CPU の実行を奪われないようにすれば問題は顕在化しないでしょう。 しかし、本当のマルチプロセッサ環境ではそのようなセコい逃げは使えません。
とはいえ、マルチスレッド対応のために特に高度なアルゴリズムが必要なわけではありません。 かなり泥臭く地道な、それこそ本当に小手先の技術が必要になります。 ただし、ある意味発想の転換が必要になるかもしれません。
これからの時代に避けて通れないこの落とし穴について、少し見てみましょう。
複数のスレッドやプロセスから読み書きアクセスするオブジェクトには、何らかの保護を考えましょう。 クリティカルセクション・ミューテックス・セマフォなど、保護に使えるさまざまな機構が用意されています。 またファイルの一部をロックする機能も OS によっては提供されています。
ここでいうオブジェクトとは、ファイル・メモリ(マルチスレッドのケース)・シェアードメモリ(またはマップドファイル・マルチプロセスのケース)・I/O ポート(あるいはソケット)などが考えられます。
要は、読まれるだけの静的オブジェクトなら保護は不要。 オブジェクトに同時にアクセスするのが設計上一人だけなら、これまた保護は不要。 しかし、同時にアクセスする可能性のあるスレッド・プロセスのうち、たった一人でも書き込む人がいるのなら(外部からの書き込みも含む --- I/O ポートなど)、そのオブジェクトのアクセスは保護する必要があります。
同期オブジェクトを使う場合に気をつけなければならないのが、デッドロックです。
デッドロックを避けるための指針がいくつかあります。
ロックをかける順番を統一する
ロックは完全なネストにする
同期オブジェクト A と B があったら、常に A を先にロックし、次に B をロックする、というようにロックする順番を統一することで、デッドロックが防げます。 デバッグ用ビルドではアサーションを使ってチェックするのもいいでしょう。
複数の同期オブジェクトを完全にネストしないで利用しても必ずしもデッドロックは生じませんが、混乱の元・バグの温床となりやすいものです。 完全にネストするという原則を守ることで、その可能性が減ります。 アサーションも書きやすくなるでしょう。
アクセスの中でも、アトミックなオペレーションそのものは同期を取って保護する必要はありません。 もちろん、他のオブジェクトと依存関係によりますが。
「アトミック」というのは、これ以上分割できないなどという意味ですが、ここでは「他の CPU から割り込まれない、結果が不定ではない(あるいは最後の人が常に勝つ)」ぐらいの意味と考えてください。
「アトミック」かどうかは、C/C++ コード上で一文に収まるかではなく、どうコンパイルされるか、CPU がどう扱うかで決まります。
x86 の場合、たとえば正しく配置された単なるワード値の読み出しはアトミックオペレーションにあたります。 あるいはメモリ上のワードを 0 に初期化することもそうです。 しかし、メモリの内容をインクリメントするのはアトミックではありません。 メモリのインクリメントは read-modify-write オペレーションに分類されます。 結果は不定になります。
アセンブリレベルでいろいろといじることが出来る場合もあるのですが(x86 の lock プレフィクスなど)、ここでは触れません。
CPU のアーキテクチャによっては単純な読み出し・書き込みがアトミックでない場合があります。
volatile を使うことで状況が好転することもありますが、万能ではありません。
メモリフェンスを意識する必要があるなど厄介です。
NT における Interlocked API のように、OS が CPU に依存しないアトミックオペレーションな API を用意している場合があります。
できるだけそちらを使うようにしましょう。
非常に簡単な例を挙げておきます。
static LONG counter; void foo() { ++counter; // 非アトミックオペレーション }
これはだめですね。 マルチスレッドセーフではありません。
上記のコードを微視的に直すなら、
static LONG counter; void foo() { InterlockedIncrement(&counter); }
となります。
もうひとつだけ例を見てみましょう。
static LONG flag; if (flag == FALSE) { flag = TRUE; ... flag = FALSE; }
このコードがまずいことはもうお分かりですね。 NT にはフラグのチェックとセットをアトミックに行える InterlockedCompareExchange がありますから、そちらを使いましょう。
if (InterlockedCompareExchange(&flag, TRUE, FALSE) == FALSE) { ... InterlockedExchange(&flag, FALSE); }
アトミックな処理をうまく使って明示的な同期オブジェクトを使わずに済めば、処理も軽くなりコードも小さくて済むなどいいことずくめかもしれません。 正しく書ければ、ですが(笑)。 ここであげたものはごく単純なケースに過ぎません。 実際にプログラムを書く段になると、かなり頭をひねる必要があったり潜在的バグの温床になったりします。 そのときは素直に同期オブジェクトを使いましょう。
余談ですが、明示的に初期化していない静的変数は、ホステッド環境では 0 に初期化されることになっています。 実装としては BSS に置かれて、初めてのアクセスでページが用意されゼロクリアされたりします。 セコいテクですが、バイナリの大きさ、実行速度とワーキングセットの縮小に貢献することがあります。 まぁ塵も積もれば…の程度ですが。
たしか かのアインシュタイン博士の名言だったと記憶していますが、プログラミングにも通用する至言です。
ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail