Cスタイル
これらは私が好きなC言語プログラミングの実践方法です。スタイルと同じくらい些細なルールもあれば、もっと入り組んだルールもあります。私はいくつかのルールには忠実に従いますが、他のルールはガイドラインとして使っています。私は、スピードよりも、正しさ、読みやすさ、シンプルさ、保守性を優先しています。なぜなら、「早まった最適化は諸悪の根源」(http://c2.com/cgi/wiki?PrematureOptimization)だからです。
正しく、読みやすく、シンプルでメンテナンス可能なソフトウェアを書き、完成したらチューニングする、ベンチマークでチョークポイントを特定する。また、最新のコンパイラは、計算の複雑さを変えることができます。配列が成長するよりもリンクリストを書く方が簡単ですが、配列のインデックスを作るよりもリストのインデックスを作る方が難しいのです。
後方互換性(例:ANSI C)は、私にとってほとんど重要ではありません。私の考えでは、後方互換性は皆の足を引っ張ります。できることなら新しい技術や新しい手法を使って、みんなを少しでも前に進ませるべきだと思うのです。
もし、ここに書かれていることに同意できなくても、それはまったく問題ありません。自分の好きなもの、自分の状況に合ったものを選んでください。これらのルールは、品質に関する普遍的な勧告を意図したものではありません。これは私の好みに過ぎず、私が行うこと、そして私が気にかけることに対してうまく機能しています。
このガイドを書くことで、私はC言語のベストプラクティスについて深く考え、再考することになりました。この文書に書かれている多くの規則について、私は何度も意見を変えました。
だから、さらに多くの点で間違っていることは間違いない。これは常に進行中の作業です。問題やプルリクエストは非常に歓迎されます。このガイドは Creative Commons Attribution-ShareAlike] の下でライセンスされていますので、あなたがこれを使って何をしようとも、私は責任を負いません。
常にすべての警告を表示した状態で開発・コンパイルする。
ここでは言い訳はしません。常に警告をオンにして開発・コンパイルしてください。しかし、-Wall
と -Wextra
は実際には「すべての」警告を有効にしないことがわかりました。他にも本当に役に立つものがいくつかあります。
CFLAGS += -Wall -Wextra -Wpedantic \
-Wformat=2 -Wno-unused-parameter -Wshadow \
-Wwrite-strings -Wstrict-prototypes -Wold-style-definition \
-Wredundant-decls -Wnested-externs -Wmissing-include-dirs
# GCC warnings that Clang doesn't provide:
ifeq ($(CC),gcc)
CFLAGS += -Wjump-misses-init -Wlogical-op
endif
また、最適化をオンにしてコンパイルすることで、エラーを検出することもできます。
CFLAGS += -O2
GCC と Clang の -M
を使って、オブジェクトファイルの依存関係を自動生成する。
GNU Make マニュアル touches に、ソースファイルの #include
s からオブジェクトファイルの依存関係を自動生成する方法について書かれています。このマニュアルに示されているルールの例は少し複雑です。以下は私が使っているルールです。
depfiles = $(objects:.o=.d)
# Have the compiler output dependency files with make targets for each
# of the object files. The `MT` option specifies the dependency file
# itself as a target, so that it's regenerated when it should be.
%.dep.mk: %.c
$(CC) -M -MP -MT '$(<:.c=.o) $@' $(CPPFLAGS) $< > $@
# Include each of those dependency files; Make will run the rule above
# to generate each dependency file (if it needs to).
-include $(depfiles)
できるだけ最新の規格で書きましょう
C11 は C99 よりも優れており、C89 よりも (はるかに) 優れています。C11のサポートはGCCとClangではまだこれからですが、多くの機能があります。もし、中期的に他のコンパイラをサポートする必要があるならば、C99に書きましょう。
常に -std=c11
のように 標準 で書いてください。gnu11
のような方言で書いてはいけない。非標準の言語拡張はなるべく使わないようにしましょう。
タブがうまく使えないので、スペースを多用する。
タブのアイデアは、インデントレベルにタブを使い、アライメントにスペースを使うというものでした。これにより、列の整列を崩すことなく、自分の好きなインデント幅を選択することができます。
int main( void ) {
|tab |if ( pigs_can_fly() == true ) {
|tab ||tab |developers_can_use_tabs( "and align columns "
|tab ||tab | "with spaces!" );
|tab |}
}
しかし、残念なことに、私たち(そして編集者)は、それが正しく行われることはほとんどありません。タブとスペースの使い分けがもたらす問題は、主に4つあります。
- インデント用のタブは、行の長さに関する意見に矛盾を生じさせます。タブ幅8を使う人は、タブ幅2を使う人よりずっと早く80文字に達してしまいます。これを避ける唯一の方法は、タブ幅を要求することですが、これではタブの利点がなくなってしまいます。
- プロジェクトごとにタブとスペースを正しく処理するようにエディタを設定するのは、スペースだけを処理するよりもはるかに困難です。こちらもご覧ください。タブ vs スペース: 永遠の聖戦
- スペースバーだけで整列するのは難しいです。スペースバーを8文字分押し続けるより、タブを2回押す方がずっと簡単です。あなたのプロジェクトの開発者は、いずれこの間違いを犯すでしょう。インデントと位置合わせにスペースを使えば、どちらの状況でもタブキーを押すことができ、素早く、簡単に、ミスが起こりにくくなります。
- スペースだけを使うプロジェクトでは、タブ/スペースのエラーを防ぐのは簡単です。なぜなら、必要なのは、タブがまったくないかどうかを検出するだけだからです。タブを使うプロジェクトで、整列のために使われるタブに対して防止するには、正規表現を考える必要があります。
複雑なものはカットし、随所にスペースを使用する。時々、他の人のインデント幅に合わせなければならないかもしれません。大変ですね。
1行の文字数が79文字を超えてはならない
79文字以上の行は絶対に書かないでください。
80文字/行は、コードを見るための事実上の標準です。この標準に依存している読者は、ターミナルやエディタを80文字幅に設定していますが、ウィンドウを横に並べれば、より多くの文字を画面に収められます。
最後の列に常にスペースがあるように、最大79文字にこだわる必要があります。こうすることで、行が次の行に続かないことがより明確になります。また、右側の余白も確保します。
80文字を超えると、80カラムの標準に依存しようとする人々にとって、あなたのコードが著しく読みにくくなります。行が折り返して読みにくくなるか、読者が最後の数文字を取得するためにウィンドウを右にスクロールしなければならなくなります。いずれにしても、自分で改行した場合よりも読みにくいコードになってしまいます。
長い行を読むと、次の行の先頭に到達するまでに目が遠くなり、さらに目が遠くなればなるほど、視覚的に再調整しなければならない可能性が高くなるからです。100字幅や120字幅は、書くのは簡単ですが、読むのは大変です。
79字を超える行があると、読者はその代償を払うことになります。79文字というのはハードリミットのようなもので、「もしも」や「でも」は許されません。長い行をどのように区切るのがベストなのか、読者はあなたに感謝することでしょう。
他の人たちがやっているように、80カラムの表示を前提に書けば、私たちはより良くなるはずです。
- Emacs Wiki: Eighty Column Rule
- Programmers' Stack Exchange: Is the 80 character limit still relevant?
どこでも //
コメントを使用し、決して /* ... を使用しないでください。*/
単一行コメントにこだわり、複雑さを軽減する。単一行コメントと比較して、複数行コメント。
- 空白のマージンと共に使用されることはほとんどないため、文字数が多くなってしまいます。
- スタイルを指定し、それに従わなければならない。
- しばしば
*/
が一行で表示されるため、行数が多くなります。 - 埋め込み
/*
や*/
に関する奇妙なルールがある - ブロック編集や拡張が難しい/できない
- は
//
よりも視覚的に邪魔になります。
インラインコメントには /*
... を使用しなければならないしかし、複数行の #define
のインラインコメントには /* ...................*/
を使わなければなりません。
#define MAGIC( x ) \
/* Voodoo magic happens here. */ \
...
しかし、私はしばしば //
コメントをマクロ本体の後に追加して、トリッキーな部分を説明することを好みます。これにより、マクロ本体が読みやすくなり、かつ(必要な)ドキュメントを提供することができると思います。
アメリカ英語プログラム
同じ言語、同じスペル、同じ語彙で開発することは重要です。これは、世界中の協力者がいるフリーソフトのプロジェクトでは特にそうです。あなたのプロジェクトでは、コード、コメント、文書において、一貫して同じ言語を使うべきです。
ですから、アメリカ英語では color
, flavor
, center
, meter
, neighbor
, defense
, routing
, sizable
, burned
, などと書きます (see more).私はオーストラリア人ですが、ほとんどのプログラマがアメリカ英語を学び、使っていることを理解しています。また、アメリカ英語のスペリングはイギリス英語よりも一貫して音声的で一貫性があります。このような理由から、イギリス英語はアメリカ英語に向かって進化する傾向があるのだと思います。
非標準ライブラリの #include
をコメントし、そこからどのシンボルを使用するかを示す
名前空間は、ソフトウェア開発の大きな進歩の一つです。残念ながら、C言語はこれに乗り遅れました(スコープは名前空間ではありません)。しかし、名前空間はとても素晴らしいものなので、コメントでそれをシミュレートしてみるべきでしょう。
#include <test.c/test.h> // Test, tests_run
#include "trie.h" // Trie, Trie_*
これにはいくつかのメリットがあります。
- 読者は、シンボルがどこで定義されているか(あるいは、以下のルールに従わない場合、シンボルがどこから来たのか)を知るために、ドキュメントを参照したり、
grep
を使うことを強制されません:あなたのコードはそれを教えてくれるだけです。 - 開発者は、どの
#include
が削除できて、どの#include
が削除できないかを判断できるようになります。 - 開発者は名前空間の汚染を考慮することを余儀なくされ、(ほとんどの C コードでは無視される) 小さな、よく定義されたヘッダのみを提供するよう奨励される
欠点は #include
コメントがチェックされない、または強制されないことです。私は長い間、このためのチェッカーを書くつもりでいましたが、今のところ、コメントが間違ってしまうのを止めるものはありません - もう使われていないシンボルに言及したり、使われているシンボルに言及しなかったりします。プロジェクトでは、このような問題の芽を摘み、蔓延を食い止めるようにしましょう。自分のコードを常に信頼できるようにすることです。 このメンテナンスは確かに煩わしいですが、#include
コメントは全体で見ればそれだけの価値があると思います。
コードベースを学習する際に、物事がどこから来たのかを見つけることは、いつも私の大きな課題の一つです。これをもっと簡単にすることができます。このように #include
コメントを書いているプロジェクトは見たことがありませんが、ぜひそうなってほしいものです。
使用するすべてのものの定義を #include
します。
ヘッダがインクルードしているものに依存しないようにしましょう。もしあなたのコードがシンボルを使うなら、そのシンボルが定義されているヘッダファイルをインクルードしてください。そうすれば、ヘッダがインクルードするものを変えても、あなたのコードは壊れません。
また、上記の #include
のコメントルールと組み合わせることで、読者や他の開発者が、あなたが使用しているシンボルの定義を見つけるために、インクルードのトレイルをたどる必要がなくなります。あなたのコードは、それがどこから来たのかを伝えるだけで良いのです。
ヘッダーの統一を避ける
なぜなら、ヘッダを統一することで、疎結合のモジュールを目的や抽象度によって明確に分離して提供する責任を、ライブラリ開発者から解放してしまうからです。たとえ開発者(のつもり)でも、統一ヘッダはコンパイル時間を増加させ、ユーザーのプログラムが必要かどうかにかかわらず、ライブラリ全体を結合してしまいます。その他にも、上記の点で触れたように、多くのデメリットがあります。
Programmers' Stack Exchange](http://programmers.stackexchange.com/questions/185773/library-design-provide-a-common-header-file-or-multiple-headers)で、統一ヘッダに関する良い解説がありました。ある回答は、GTK+のようなものが単一のヘッダーファイルしか提供しないのは合理的であると述べています。私は同意しますが、それはGTK+の設計が悪いからであり、グラフィカルツールキットに内在するものではありません。
ユーザーが型を書くのが難しいのと同じように、ユーザーが複数の #include
を書くのは難しいのです。そこに難しさを持ち込むことは、木を見て森を見ずです。
すべてのヘッダーにインクルードガードを設け、二重インクルードを防止する。
インクルードガード は、コンパイルを中断することなく、ヘッダーファイルを「2回」インクルードできるようにします。
// Good
#ifndef INCLUDED_ALPHABET_H
#define INCLUDED_ALPHABET_H
...
#endif // ifndef INCLUDED_ALPHABET_H
Rob Pikeはインクルードガードに反対しています。インクルードファイルの中に決してファイルを含めないようにすればいいと言っています。彼は、インクルードガードは依然として「字句解析器を通過する何千行もの不要なコードをもたらす」と述べています。
実際、GCCはインクルードガードを検出します。そして、そのようなファイルを二度読みすることはありません。ほかのコンパイラがこの最適化を行うかどうか、わたしは知りません。
わたしは、ユーザがあなたのヘッダファイルの依存関係をインクルードすることを要求するのは良い考えだとは思いません。あなたのヘッダーファイルの依存関係は、本当は「公開」とみなされるべきではないのです。ヘッダファイルがインクルードしているものには依存しない」というルールを強制することになりますが、ヘッダファイルが FILE
や bool
のような必要ないものを使っているとすぐに崩れてしまいます。ユーザは、自分が必要としないのであれば、そんなことを気にする必要はないはずです。
ですから、常にインクルードガードを書き、ユーザを楽にしてあげましょう。
大きな条件部分は必ず #endif
をコメントする。
グローバル変数やスタティック変数の使用は避けてください。
グローバル変数は、それを使用するすべての関数の隠れた引数に過ぎません。そのため、関数が何をしているのか、どのように制御されているのかを理解するのがとても難しくなっています。
Mutable なグローバル変数は特に悪であり、何としてでも避けるべきものです。概念的には、グローバル変数の代入は、隠された静的な変数を設定するための longjmp
の束なのです。うっそー。
関数は常に引数で完全に制御できるように設計することを心がけなければなりません。たとえ多くの関数に渡さなければならない変数があったとしても、それが関数の計算に影響を与えるのであれば、それは引数か引数のメンバであるべきです。これは、常に、より良いコードとより良い設計につながります。
例えば、私の Trie.c プロジェクトからグローバル変数と定数を削除した結果、Alphabet
構造体が生まれました。また、同じTrieに対してアルファベットを瞬時に交換するような、とてもクールな動的能力も生まれました。
関数内の静的変数は、その関数にスコープされたグローバル変数に過ぎず、上記の引数はそれらにも等しく適用されます。グローバル変数と同様に、静的変数もモジュール化された純粋な関数を提供することから逃れるための簡単な方法としてよく使われます。静的変数は、パフォーマンスという名目で擁護されることがよくあります (ベンチマークが先です!)。グローバル変数が不要なのと同じように、スタティック変数も不要です。永続的な状態が必要なら、関数にその状態を引数として受け取らせればいい。永続的なものを返す必要があるなら、そのためのメモリを確保する。
公開するものを最小限にし、トップレベルの名前を static
と宣言して、それが可能な場所に置く。
ヘッダーファイルは、ユーザがライブラリを使用するために必要なものだけを含めるようにします。内部関数や構造体、マクロはここで提供するべきではありません。複数のソースファイルの間で必要な場合は、内部ヘッダーファイルを用意します。
関数やグローバル変数がヘッダでエクスポートされない場合、ソースファイル内で static
と宣言し、内部リンクを与える。これにより、オブジェクトファイル間の名前の衝突がなくなり、いくつかの最適化が可能になり、リンク速度が向上します。
不変性が命を救う: できる限り const
を使おう
const` はコンパイル時の正しさを向上させます。これは、読み取り専用のポインタを文書化するためだけではありません。すべての読み取り専用変数とポインタのために使用されるべきです。
const
は、読者が機能の一部を理解する上で、 非常に 役立ちます。初期化を見て、その値がスコープ全体で変化しないことを確認できれば、スコープの残りの部分についてより簡単に推論することができます。何が変更され、何が変更されないかを理解するために、読者はスコープ全体を理解することを余儀なくされます。もしあなたが一貫して const
を使っていれば、読者はあなたを信頼し始め、 const
で修飾されていない変数は、スコープのどこかの時点で変更されるシグナルであると仮定できるようになります。
あらゆるところで const
を使用することは、開発者として、プログラムの制御フローで何が起こっているのか、どこで変異が広がっているのかを推論するのにも役立ちます。特にポインターやポインティーに関しては、const
を使うと、コンパイラーがどれだけ助けてくれるかは驚くべきことです。コンパイラは常にあなたの味方でありたいものです。
コンパイラは関数呼び出しの際にポインティが const
ness を失うと警告を出しますが、ポインティが const
ness を得ても文句は言いません。したがって、ポインタの引数が読み取り専用である場合に const
として指定しなければ、ユーザが自分のコードで const
を使用するのを阻止することになります。
// 悪い点:sumはその配列をconstとして定義する必要がある。
int sum( int * xs, int n );
// なぜなら、そうしないとコンパイル時の警告になるからです。
int const xs[] = { 1, 2, 3 };
return sum( xs, sizeof xs );
// => 警告: 引数 2 の 'sum' を渡すと、ポインタのターゲット型から 'const' 修飾子が削除されます。
したがって、少なくとも関数のシグネチャについては、 const
を使うことはあまり選択肢ではありません。多くの人が const
を使うことは有益だと考えているので、好むと好まざるとにかかわらず、誰もが const
を使うことを必須と考えるはずです。もし const
を使わないのであれば、ユーザは関数へのすべての呼び出しをキャストするか (ヤバイ)、 const
の警告を無視するか (面倒くさい)、 const
修飾子を取り除くか (コンパイル時の正しさを失う) のいずれかを迫られることになるのです。
もし、 const
を無視するようなライブラリを使わざるを得ない場合は、代わりにキャストしてくれるマクロを書けばいいのです。
// `sum` は指定された配列を変更しない。 `const` ポインタに対してはキャストする。
#define sum( xs, n ) sum( ( int * ) xs, n )
関数プロトタイプのポインティには const
修飾子のみを与える - 引数名自体への const
は単なる実装の詳細でしかない。
// Unnecessary
bool Trie_has( Trie const, char const * const );
// Good
bool Trie_has( Trie, char const * );
残念ながら、C 言語は const でない Pointee-pointees から const の Pointee-pointees への変換を扱うことができません。したがって、点状体を const
にしないことをお勧めします。
char ** const xss = malloc( 3 * ( sizeof char * ) );
char const * const * const yss = xss;
// 警告: 互換性のないポインタ型からの初期化
char * const * const zss = xss;
// <no warning>
もし、内部構造体のポインティーをconst
にできるのであれば、そうしてください。ポインティが一定でないと、ミュータビリティが不必要に広がってしまい、残りの const
修飾子から情報を得ることが難しくなります。内部構造体を完全に制御することができるので、将来的に const
を削除する必要がある場合は、削除することができます。
通常、外部構造体のポインティは const
するべきではありません。パブリックインターフェイスの一部である場合は、柔軟性が重要です。よく考えてみてください。私がよくやる例外は、 error
フィールドのような、文字列リテラルに代入するのが最適なフィールドの場合です。この場合、 char const *
型を使用することで、あなたやあなたのユーザーが、セグメンテーションフォールトを引き起こすような文字列リテラルを変更することを防ぐことができます。
構造体フィールドの pointees を const
するのは合理的ですが、構造体フィールドそのものを const
するのは決して有益ではありません。例えば、その構造体の値を malloc
(http://stackoverflow.com/questions/9691404/how-to-initialize-const-in-a-struct-in-c-with-malloc) するのが面倒になります。もし、フィールドが元の値以上に変化しないようにすることが本当に意味があるなら、必要な品質を強制する invariants を定義すればいいのです。また、構造体の個々の変数を const
として定義することで、同じ効果を得ることができます。
リターン型のポインタを const
にするのは、必要な場合だけ、そして慎重に検討した後だけにしてください。コンパイラが戻り値の型に const
を追加するようにほのめかしているとき、それは const
を追加するのではなく、どこかで remove するべきだという意味であることが多いことに気がつきました。これは柔軟性を損なう可能性があるので、注意が必要です。
最後に、型キャストやポインタを使用して const
修飾子を回避することは、少なくとも自分がコントロールする場合には、絶対にしないでください。もし変数が定数でないなら、定数にしないでください。
常に const
を右に置き、型を右から左に読んでいく
const char * word; // 悪い点:コンスト的でないこと
const char * const word; // 悪い点:文字が非常に読みづらくなる
char const* const word; // 悪い点:配置が変
// 良好:右から左へ、ワードは定数charへの定数ポインタ
char const * const word;
このルールのため、*
型修飾子は常に空白で埋める必要があります。
関数のプロトタイプに引数名を書かない(型を繰り返すだけ)。
しかし、ポインタの引数が配列へのポインタ(複数形)か値へのポインタ(単数形)かを伝えるために、常にその名前を宣言してください。
bool trie_eq( Trie trie1, Trie trie2 ); // 悪い
bool trie_eq( Trie, Trie ); // 良い
// 悪い点 - これらは修正用のポインタなのか、ヌルなのか、それともアレイなのか?
void trie_add( Trie const *, char const * );
// 良い
void trie_add( Trie const * trie, char const * string );
特別な理由がない限り、 float
ではなく double
を使用する。
Ben Klemens著「21st Century C」より。
printf( "%f\n", ( float )333334126.98 ); // 333334112.000000
printf( "%f\n", ( float )333334125.31 ); // 333334112.000000
最近のアプリケーションの大部分では、スペースは問題になりませんが、浮動小数点数のエラーは依然として脅威となります。フロートよりもダブルの方が、数値のドリフトが問題になりにくいのです。よほど特殊な理由がない限り、 float
s を使用する代わりに double
s を使用してください。なぜなら、ベンチマークを行わないと、それが実際に目に見える違いを生むかどうかわからないからです。開発を終えてから、ベンチマークを実施してチョークポイントを特定し、その部分で float
s を使って、それが実際に役に立つかどうかを見てください。その前に、パフォーマンスの向上よりも他のことを優先してください。早まって最適化しないこと。
変数の宣言はできるだけ遅くする
変数を使用する場所で宣言することで、読者は自分が扱っている型について思い出すことができます。また、変数のスコープを最小にするために、関数を抽出する場所を示唆します。さらに、それぞれの変数がどこに関係しているのかを読者に知らせることができます。必要なときに変数を宣言すると、ほとんどの場合、単なる宣言(int x;
)ではなく初期化(int x = 1;
)を行うことになります。変数を初期化するということは、たいていの場合、その変数を const
することもできるということです。
私にとって、すべての宣言(=非初期化)はシビれるものです。
変数の定義は1行にまとめる。
これは、アトム行の編集が容易なため、将来的にタイプを変更することが容易になります。もし、すべての型を一緒に変更する必要がある場合は、エディタのブロック編集モードを使用する必要があります。
構造体の定義は、アクティブなコードよりも理解しやすいので、意味的につながりのある構造体メンバを束ねるのはいいと思いますけど。
// ファイン
typedef struct Color {
char r, g, b;
} Color;
短い変数名を恐れない
もしスコープが画面に収まり、その変数が多くの場所で使われていて、それを表すのに明らかな文字が1つか2つあれば、それを試してみて、可読性を高めることができるかどうか見てみてください。きっと読みやすくなりますよ
関数間で変数名を統一する
一貫性を持たせることで、読者は何が起こっているのか理解しやすくなります。関数内で同じ値に対して異なる名前を使うのは怪しく、読者に重要でないことを推論させることになります。
ブール値を使用する場合は、stdbool.h
の bool
を使用します。
int print_steps = 0; // 悪いこと - これは歩数を数えているのでしょうか?
bool print_steps = false; // 良い - 意図が明確である
真偽に頼らず、明示的に価値を比較する。
明示的な比較は、C言語では必ずしも明らかでないため、読者に何を扱っているのかを教えてくれます。それはC言語では必ずしも明らかではないからです。C言語で真偽を問われる変数を見たとき、私が最初にすることは、その型を見つけるために宣言を探し出すことです。プログラマーが比較で教えてくれればよかったのにと本当に思います。
// 悪い点 - これらの表現は実際に何をテストしているのか(もしそうなら)?
if ( on_fire );
return !character;
something( first( xs ) );
while ( !at_work );
// 良い - 情報を提供し、曖昧さをなくす。
if ( on_fire > 0 );
return character == NULL;
something( first( xs ) != '\0' );
while ( at_work == false );
私は、 is_edible
や has_client
のような述語として名付けられたブーリアン関数については、このルールをスキップすることがよくあります。しかし、私は通常、このような状況では == true
や == false
のような視覚的な混乱は、読者の助けになるというよりも、むしろ面倒なものだと考えています。あなたの判断でどうぞ。
式の中で状態を変化させない(例:代入や++
など)
読みやすい(命令形)プログラムは、右から左ではなく、上から下へ流れます。残念ながら、C言語プログラミングでは、このようなことが多すぎるのです。この習慣と慣習はThe C Programming Languageによって始められ、それ以来、多くの文化に根付いているのだと思います。これは本当に悪い習慣で、プログラムが何をしているのかを追うのがとても難しくなります。式の中で状態を変更してはいけません。
trie_add( *child, ++word ); // 悪い
trie_add( *child, word + 1 ); // 良い
// よかった、もし `word` を修正する必要があるのなら
word += 1;
trie_add( *child, word );
// 悪い
if ( ( x = calc() ) == 0 );
// 良い
x = calc();
if ( x == 0 );
// 技術的には式中の代入
a = b = c;
while ( --atoms > 0 ); // 悪い
while ( atoms -= 1, // 良い
atoms > 0 );
// いいんです、これ以上の方法はない、繰り返さないで
int w;
while ( w = calc_width( shape ),
!valid_width( w ) ) {
shape = reshape( shape, w );
}
変数の値が意味的にリンクしていない限り、多重代入は使わないでください。もし、偶然にも同じ値を持つ2つの変数が近くにあったとしても、行を節約するためだけにそれらを多重代入に放り込まないようにしましょう。
カンマ演算子は、上記のように慎重に使用してください。できれば、カンマは使わないでください。
// 悪い
for ( int i = 0, limit = get_limit( m ); i < limit; i += 1 ) {
...
}
// より良い
int const limit = get_limit( x );
for ( int i = 0; i < limit; i += 1 ) {
...
}
式中の純粋でない関数呼び出しや自明でない関数の呼び出しを避ける
たとえ変数が int result
のような単純なものであっても、関数呼び出しを変数に割り当てて、それが何であるかを記述します。こうすることで、条件文の中に隠された純粋でない関数による状態の変化で読者を驚かせることを避けることができます。私にとっては、if ( ... )
の中の式が外界の状況を変化させると考えるのは実に不自然なことなのです。その状態変化の結果を変数に代入して、その値をチェックする方がよっぽどわかりやすい。
たとえそれが当たり前のことで、行数を減らすことができると思っていても、失敗する可能性があることには価値がありません。このルールを守って、何も考えないでください。
関数名が is_adult
や in_tree
のような述語で、条件文脈で自然に読めるのであれば、結果の代入を省略してもいいと思うんだ。必要であれば、このような関数をブール式で結合することもできますが、ご自分の判断でお願いします。複雑なブーリアン式は、関数に抽出することが多いはずです。
// 良い
int r = listen( fd, backlog );
if ( r == -1 ) {
perror( "listen" );
return 1;
}
// 良い
if ( is_tasty( banana ) ) {
eat( banana );
}
単一ステートメントブロックであっても、必ず括弧を使用します。
安全で、変更しやすく、一貫性があるので読みやすいという理由から、常に括弧を使用します。同じ理由で、一行文を条件と同じ行に書かないようにしましょう。
以下は、The C Programming Languageの実際のコードです。こんなことはしないでください。
while (--argc > 0 && (*++argv)[0] == '-')
while (c = *++argv[0])
switch (c) {
...
}
if (argc != 1)
printf("Usage: find -x -n pattern\n");
else
while (getline(line, MAXLINE) > 0) {
...
}
整数の変換規則が複雑なため,符号なし型を避ける
CERTは整数変換規則を説明しようとすると述べている。
整数変換規則を誤解していると、エラーが発生し、それが脆弱性の悪用につながる可能性があります。深刻度:中、可能性:あり。
ANSI標準を探求する素晴らしい本であるExpert C Programmingも、その最初の章でこのことを説明しています。ここから得られるのは、「負の値であってはならないからと言って、 unsigned
変数を宣言してはいけない」ということです。もし、より大きな最大値が欲しいなら、 long
か long long
(次のサイズ) を使ってください。
もし関数が負の数で失敗するなら、それはおそらく大きな数でも失敗するでしょう。もし関数が負の数で失敗するなら、正の数であると断言すればよいのです。多くの動的言語では、どちらの符号にも対応できる単一の整数型が使用されていることを思い出してください。
符号なし値は型安全性を提供しません。-Wall
と -Wextra
を使っても、GCC は unsigned int x = -1;
に目をつぶってくれません。
また、Expert C Programming では、符号なし値に評価されるすべてのマクロをキャストする理由についての例を示しています。
#define NELEM( xs ) ( ( sizeof xs ) / ( sizeof xs[0] ) )
int const xs[] = { 1, 2, 3, 4, 5, 6 };
int main( void )
{
int const d = -1;
if ( d < NELEM( xs ) - 1 ) {
return xs[ d + 1 ];
}
return 0;
}
NELEM
は (sizeof
によって) unsigned int
と評価されるため、if
ブランチは実行されません。そのため、d
は unsigned int
に昇格します。2 の補数](https://en.wikipedia.org/wiki/Two's_complement) の -1
は可能な限り大きな符号なし値 (ビット単位) を表すので、この式は false となり、プログラムは 0
を返します。この場合の解決策は、 NELEM
の結果をキャストすることです。
#define NELEM( xs ) ( long )( ( sizeof xs ) / ( sizeof xs[ 0 ] ) )
よく定義されたビット演算とモジュール算術のオーバーフローを提供するために、符号なし値を使用する必要があります。しかし、これらの値を保持し、符号付き値と相互作用させないようにします。
+= 1
と -= 1
を ++
と --
よりも使用する。
実は、できればどちらの形式も使わないでください。状態を変更することは常に避けるべきです (無理のない範囲で)。しかし、どうしても必要な場合には、 +=
と -=
は ++
や --
よりも明白でシンプル、かつ暗号化されておらず、他の文脈や他の値でも役に立ちます。また、 +=
と -=
の評価にはトリックがなく、別の評価を提供するための奇妙な双子の演算子もありません。Pythonには ++
と --
演算子がありませんし、Douglas CrockfordはそれらをJavaScriptのGood Partsから除外しました。このルールに従うことで、式の中で状態を変化させないようにすることもできます。
演算子の優先順位が明らかでない式には、括弧を使用する
int x = a * b + c / d; // 悪い
int x = ( a * b ) + ( c / d ); // 良い
&sockaddr->sin_addr; // 悪い
&( sockaddr->sin_addr ); // 良い
よく見かける操作の組み合わせについては、例外を設けることができますし、そうすべきです。例えば、等号演算子とブール演算子を組み合わせる際に演算子を省略することは問題ありません。なぜなら、読者はおそらくそれに慣れていて、その結果に自信を持っているからです。
// ファイン
return hungry == true
|| ( legs != NULL && fridge.empty == false );
switch
を使わず、複雑な条件文も避ける。
スイッチのフォールスルーメカニズムはエラーが起こりやすく、ケースをフォールスルーさせることはほとんどないので、ほとんどのスイッチのコードは if
と同等のものより長くなります。さらに悪いことに、break
がなくてもコンパイルすることができます。私が switch
を使っていたときは、いつもこれでつまづいていました。また、 case
の値は積分定数式でなければならないので、他の変数にマッチすることはできません。このため、ロジックを関数に展開することができません。さらに、switch
の中のどの文もラベル付けしてジャンプすることができるので、例えば defau1t
とタイプミスしたときに非常にわかりにくいバグを助長します。
もし、異なる定数値を動作に対応させる必要がある場合、次のようになります。
switch ( x ) {
case A:
do_something_for_a( x, y, z );
break;
case B:
do_something_for_b( x, y, z ):
break;
default:
error( x, y, z );
break;
}
// これらの関数は明示的な関数ではないかもしれません(つまり、これらの変数のいくつかを使用する一連のステートメントであるかもしれません)。
より明示的でテスト可能かつ再利用可能なアプローチとしては、3項式を用いて正しい型の関数ポインタを返す関数を定義することです。
action_fn get_x_action( x ) {
return ( x == A ) ? do_something_for_a
: ( x == B ) ? do_something_for_b
: error;
}
action_fn action = get_x_action( x );
action( x, y, z );
// または単に
get_x_action( x )( x, y, z );
// `action` はひどい名前で、例として使われているに過ぎません。もっと情報量の多い名前を考えてみてください。
このように、相関のない定数値の2つのセットの間でマッピングする必要がある場合にも、同様のことをする必要があります。
// 悪い
switch ( x ) {
case A: return X;
case B: return Y;
case C: return Z;
default: return ERR;
}
// 良い
return ( x == A ) ? X
: ( x == B ) ? Y
: ( x == C ) ? Z
: ERR;
ブーリアン式で済むところを switch
を使わないでください。
// 悪い
switch ( x ) {
case A: case B: case C:
return true;
default:
return false;
}
// 良い
return x == A || x == B || x == C;
// あるいは、名前が長い場合は、通常、この方が読みやすい。
return t == JSON_TYPE_null
|| t == JSON_TYPE_boolean
|| t == JSON_TYPE_number;
このように、switch
のフォールスルー動作が必要な場合。
switch ( x ) {
case A:
// Aのもの、Bに落ちる
case B:
// B stuff
break;
default:
// デフォルトのもの
}
これと同等の if
はもっと読みやすく、何がどうして起こるのかが一目瞭然です。Bに関すること」は実際には x == A
の場合にも適用され、これは if
を使用する際に明示的に宣言されます。
if ( x == A ) {
// A stuff
}
if ( x == A || x == B ) {
// B stuff
} else {
// default stuff
}
switch
を使う必要があるのは、パフォーマンスチューニングのときだけです (ベンチマークでホットスポットを特定したらね!)。そうでなければ、より安全で、より短く、よりテストしやすく、再利用可能な代替案が常にあります。
関数と構造体の定義を2行で区切る
関数内の空行を最大1行に制限すれば、このルールによってグローバルな要素を視覚的に明確に分離することができます。これは、PythonのPEP8スタイルガイドから学んだ習慣です。
変数のスコープを最小にする
もし、いくつかの変数が連続した行の中でしか使われず、その行の後に一つの値しか使われないのであれば、それらの最初の行は関数に抽出するのに最適な候補となります。
// Good: addrはhandle_requestの最初の部分でのみ使用された。
int accept_request( int const listenfd )
{
struct sockaddr addr;
return accept( listenfd, &addr, &( socklen_t ){ sizeof addr } );
}
int handle_request( int const listenfd )
{
int const reqfd = accept_request( listenfd );
// ... addrに関係なく、reqfdに関係するもの。
}
もし accept_request
のボディが handle_request
に残っていた場合、 addr
変数は reqfd
を取得するためだけに使用されるにもかかわらず、 handle_request
関数の残りの部分でスコープ内に存在することになります。このようなことは、関数を理解する上での負担になるので、可能な限り修正されるべきです。
変数の露出を制限するもう一つの戦術は、複雑な式を次のようにブロックに分割することです。
// というより、むしろ
bool trie_has( Trie const trie, char const * const string )
{
Trie const * const child = Trie_child( trie, string[ 0 ] );
return string[ 0 ] == '\0'
|| ( child != NULL
&& Trie_has( *child, string + 1 ) );
}
// childは条件分岐の後半にしか使われないので、次のように露出を制限することができます。
bool trie_has( Trie const trie, char const * const string )
{
if ( string[ 0 ] == '\0' ) {
return true;
} else {
Trie const * const child = Trie_child( trie, string[ 0 ] );
return child != NULL
&& Trie_has( *child, string + 1 );
}
}
単純な定数式は変数よりも読みやすい場合がある
定数式にしか代入されない変数を、その定数式に置き換えると、コードの可読性が向上することがよくあります。
上の trie_has
の例を考えてみましょう。string[ 0 ]
式が2回繰り返されています。もし、char
変数を定義するために余分な行を挿入したら、読むのも追うのも大変になるでしょう。これは、読者が念頭に置かなければならないもう一つのことなのです。他の言語の多くのプログラマーは、配列のアクセスを繰り返すことを考えもしないでしょう。
余計な変数より複合リテラルを優先する
これは、変数の範囲を最小化するのと同じ理由で有益です。
// 悪いことに、`sa` が二度と使われないとしたら。
struct sigaction sa = {
.sa_handler = sigchld_handler,
.sa_flags = SA_RESTART
};
sigaction( SIGCHLD, &sa, NULL );
// 良い
sigaction( SIGCHLD, &( struct sigaction ){
.sa_handler = sigchld_handler,
.sa_flags = SA_RESTART
}, NULL );
// 悪い
int v = 1;
setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, &v, sizeof v );
// 良い
setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, &( int ){ 1 }, sizeof int );
for
のような制御構造をラップするマクロを使用したり、提供したりしてはいけません。
データ構造の要素をループするマクロは、余計な構文があり、読者は定義を調べないと制御の流れが分からないので、非常に紛らわしいです。
プログラムを理解するためには、読者がその制御フローを理解できることが重要です。
コントロールマクロはオプションとしてでも提供しないでください。それらは普遍的に有害なので、有効にしないでください。ユーザーが本当に望むなら、自分で定義すればいいのです。
// 悪い
#define TRIE_EACH( TRIE, INDEX ) \
for ( int INDEX = 0; INDEX < ( TRIE ).alphabet.size; INDEX += 1 )
// ここで実際に何が起こるのか、まったく予想がつきません。
TRIE_EACH( trie, i ) {
Trie * const child = trie.children[ i ];
...
}
マクロが関数呼び出しと異なる動作をする場合のみ、大文字にします。
「異なる動作」というのは、ユーザーが予期しないときに物事が壊れるかどうかということです。マクロの見た目が違うだけなら(例:名前付き引数のテクニック)、大文字の名前を正当化する理由にはならないと思います。マクロに大文字の名前を付けるべきは、次のような場合です。
- はその本体で引数を繰り返しますが、これは純粋でない式では壊れてしまうからです。多くのコンパイラはこれを防ぐためにstatement expressionsを提供していますが、これは非標準的です。ステートメント式を使用する場合、マクロ名を大文字にする必要はありません。
- がブロックや制御構造でラップされている場合、式として使用できないからです。
- は、例えば
return
やgoto
などで、周囲のコンテキストを変更します。 - は名前付き引数として配列リテラルを取ります。(なぜ)
マクロが関数に固有のものである場合は、本文中で #define
してください。
変数のスコープを常に最小にするのと同じ理由で、マクロのスコープを制限することが理にかなっている場合は、そうすべきです。
文字列を配列として初期化し、バイトサイズに sizeof
を使用する。
文字列リテラルは常に配列として初期化することで、sizeof str
だけでバイトサイズを取得できるからです。ポインタとして初期化した場合は、strlen( str ) + 1
でバイトサイズを取得しなければなりません。
// 良い
char const message[] = "always use arrays for strings!";
write( output, message, sizeof message );
また、ポインタの初期化は配列の初期化よりも安全ではありません。 *ただし、文字列リテラルを char const *
型で初期化するように -Write-strings
を指定してコンパイルする場合は例外です。残念ながら、 -Wwrite-strings
は -Wall
や -Wextra
に含まれていないため、明示的に有効化する必要があります。 Write-stringsがなければ、文字列リテラルを
char *` に代入することができます。しかし、そのポインタの要素を再割り当てすると、プログラムがセグメンテーションフォールトを起こします。
// Write-stringsを指定しない場合、警告を出さずにコンパイルできますが、2行目でsegmentation faultが発生します。
char * xs = "hello";
xs[ 0 ] = 'c';
// このプログラムは、正常にコンパイル・実行されます。
char xs[] = "hello";
xs[ 0 ] = 'c';
文字列リテラルをポインタとして初期化することの利点は、これらのポインタが読み取り専用のメモリを指すようになるため、いくつかの最適化が可能になる可能性があることです。文字列リテラルを配列として初期化すると、基本的に変更可能な文字列が作成されます。これは、 const
による変更に対して「人為的に」保護されているだけですが、これはキャストで破ることができます。
繰り返しになりますが、早まった最適化をしないことをお勧めします。開発を終えてベンチマークを行うまでは、パフォーマンスを最も優先させるべきです。文字列リテラルの定義に関するテストは見たことがありませんが、文字列リテラルをポインタとして定義することによって、顕著な速度向上が見られるのは非常に驚きです。
すべてのものをconst
するルールで述べたように、const
を決して捨ててはいけません。代わりに const
を削除してください。人工的な」保護については心配しないでください。私は、定数値は、違反するとセグメンテーション違反になるような暗黙のルールよりも、コンパイル時に警告が出るような明示的な構文構造で保護されている方がずっといいと考えています。
最後に、配列の初期化にこだわることで、ポインタの初期化と配列の初期化を、ミュータビリティが必要かどうかによって切り替えるという概念上のオーバーヘッドを、あなたや読者に節約させることができるのです。
ただ、文字列リテラルは常に配列として初期化し、シンプルに保つようにします。
可能であれば、型ではなく sizeof
を変数に使用します。
そうすれば、後で変数の型を変更しても、一度だけ変更すればよいのです。常に正しいサイズを得ることができるのです。
// Good
int * a = malloc( n * ( sizeof *a ) );
複合リテラルではできませんが。一度しか使わない変数を削除するのは、トレードオフとして価値があると思います。
setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, &( int ){ 1 }, ( sizeof int ) );
関数の引数定義に配列構文を使用しない
配列はほとんどの式でポインタになるし、関数への引数として渡される場合も含まれる。関数は決して配列を引数として受け取ることはできません。配列へのポインタのみです。sizeof` は配列の引数宣言のようには動作しません:ポインタのサイズを返すのであって、指された配列を返すのではありません。
関数の引数で静的配列インデックスを指定するのはいい が、リテラルに NULL
が与えられたときのような非常につまらない状況に対してしか保護されないのです。また、GCCはその違反について警告しませんがまだ、Clangだけは警告します。私は紛らわしいとは思っていません。non-obvious syntax to be worth the small compilation check.
そう、[]
は引数が配列として扱われることを示唆しますが、requests
のような複数形の名前も同様なので、その代わりにそうしてください。
ポインタ演算よりも配列インデックスを常に優先する
配列を扱う場合は、配列として扱います。ポインタの演算は混乱しやすく、バグも発生しやすいものです。配列のインデックスにこだわることで、重要な変数だけを一定に保ち、インデックス変数だけを非一定にすることができます。
// 悪い
for ( ; *str != '\0'; str += 1 );
// 良い
for ( int i = 0; str[ i ] != '\0'; i += 1 );
構造体の不変量を文書化し、不変量チェッカーを提供する。
不変量**とは、プログラムの実行中に真であることが当てになる条件である。
構造体(または構造体へのポインタ)を受け取る関数では、その構造体のすべての不変量が、関数の実行前と実行後に真になる必要があります。不変量は、呼び出し側には有効なデータを提供する責任があり、関数側には有効なデータを返す責任があることを意味します。不変量によって、これらの関数はこれらの条件のアサーションを繰り返す必要がなくなり、さらに悪いことには、無効なデータをチェックし、処理することさえしなくなります。
構造体の定義の最後に「invariants」というコメント欄を設け、思いつく限りの不変量を列挙してください。また、ユーザーが自分で作成した構造体の値に対して、これらのアサーションをチェックできるように is_valid
と assert_valid
関数を実装してください。これらの関数は、構造体の値に対して不変量が成立していることを信頼できるようにするために重要です。これがなければ、ユーザーはどうやって知ることができるのでしょうか?
構造体不変量の一例です。
私の大学の教員は、ソフトウェアの正しさについてかなり大きな存在です。確かに、その影響は受けています。
さもなければプログラムが失敗するような場所では、assert
を使用する。
データを削除したり、セキュリティの脆弱性を防いだり、セグメンテーションフォールトを防いだりと、プログラムが愚かなことをする前に、意味のあるクラッシュをするためにアサーションを書きましょう。良いソフトウェアは速く失敗する。
もし関数にポインタが与えられたら、それをデリファレンスして、それがヌルでないことを保証します。配列のインデックスが与えられたら、それが範囲内であることを確認します。引数間の整合性が必要な場合は、それを保証すること。
とはいえ、決してアサーションに頼ってはいけません。アサーションの行を削除しても、プログラムは正しく動作するはずです。
アサーションをエラー報告だと勘違いしないようにしましょう。そうでなければわざわざチェックしないようなことをアサーションしましょう。もし (コードではなく) ユーザーの入力がアサーションを無効にしてしまったら、 それはバグです。事前にフィルタリングを行い、ユーザーにとって読みやすい形でエラーを報告すべきです。
assert
を繰り返し、一緒に&&
してはいけない
assert
を繰り返し呼び出すと、アサーションエラーのレポートが改善されます。アサーションを &&
で連結すると、どの条件が失敗したのかがわからなくなります。
可変長配列の使用禁止
可変長配列は、動的な長さの配列を自動で保存する方法として C99 で導入されたもので、 malloc
は必要ありません。いくつかの理由により、C11 ではオプションとなりました。したがって、C11 で可変長配列を使用したい場合は、いずれにせよ malloc
バージョンを書かなければなりません。その代わり、可変長配列を使わないようにすればいいのです。
C99でも可変長配列を使うのはやめたほうがいいと思います。まず、スタックスマッシュを防ぐために、サイズを制御する値をチェックする必要があります。次に、初期化できないことです。最後に、これらを避けることで、後で新しい標準にアップグレードすることが容易になります。
型安全性を損なうので void *
を避ける。
void *
はポリモーフィズムのために有用ですが、ポリモーフィズムが型安全性と同じくらい重要であることはほとんどありません。ボイドポインタは多くの場面で必要不可欠なものですが、まずは他の安全な代替手段を検討すべきです。
もし void *
があれば、できるだけ早く型付き変数に代入してください。
初期化されていない変数を扱うのが危険なように、void ポインタを扱うのも危険です:コンパイラを味方につけたいところです。ですから、できるだけ早く void *
を捨ててください。
相互排他的なフィールドではなく、C11の匿名構造体とユニオンを使用する
あるフィールドが特定の値であるときに、あるフィールドだけを設定したい場合は、C11の匿名構造体やユニオンを使用します。
enum AUTOMATON_TYPE {
AUTOMATON_TYPE_char,
AUTOMATON_TYPE_split,
AUTOMATON_TYPE_match
};
#define NUM_AUTOMATON_TYPES ( 3 )
typedef struct Automaton {
enum AUTOMATON_TYPE type;
union {
struct { // type = char
char c;
struct Automaton * next;
};
struct { // type = split
struct Automaton * left;
struct Automaton * right;
};
};
} Automaton;
というようなものよりも、ずっと明快でわかりやすい。
typedef struct Automaton {
enum AUTOMATON_TYPE type;
char c;
struct Automaton * left;
struct Automaton * right;
} Automaton;
必要なとき以外はタイプキャストしない(おそらくしないでしょう)
ある型の値を別の型の変数に代入することが有効なら、キャストする必要はない。というように、必要なときだけ型キャストを使えばいいのです。
- int` 式の真の除算(整数の除算ではない)の実行
- 配列のインデックスを整数にすること。
- 構造体や配列に複合リテラルを使用する。
// これで問題なくコンパイルできます。
struct Apple * apples = malloc( sizeof *apples );
構造体に TitleCase という名前をつけて typedef する。
// 良い
typedef struct Person {
char * name;
int age;
} Person;
構造体には大文字と小文字を区別する必要があり、 struct
という接頭辞がなくても認識できるようにします。また、構造体の変数に同じ名前を付けても、名前が衝突することがありません (たとえば、 Banana
型の Banana
など)。構造体の名前は、後で必要になるかもしれないので、必要ない場合でも常に定義しておくべきです (たとえば、不完全な型として使用する場合など)。また、名前が先頭にあると、コメントが挿入されたときや、構造体の定義が大きくなったときに、読みやすくなります。
ただし、名前付き引数に使う構造体(後述)は、TitleCase の命名が変になるので、typedef しない。とにかく、名前付き引数にマクロを使うのであれば、typedef は不要で、struct の定義が隠れることになります。
もし、構造体を型付けすることが嫌いな場合は、代わりに struct
ネームスペースを使用することができます(これは公平です。
typedef構造体のみ。基本型やポインタは不可。
// 悪い
typedef double centermeters;
typedef double inches;
typedef struct Apple * Apple;
typedef void * gpointer;
この間違いは非常に多くのコードベースが犯しています。これは実際に何が起こっているかを隠してしまい、ドキュメントを読むか、 typedef
を見つけて、それをどう扱うかを学ばなければなりません。自分のインターフェイスでは決してこのようなことをせず、他のインターフェイスの型定義は無視するようにしましょう。
これらの批判は、上でアドバイスしたように、構造体の型付けにも同様に当てはまります。私見では、すべての struct
宣言を削除することで得られる視覚的な明瞭さは、ユーザーにこの規約を認識させる(あるいは実現させる)ことに値すると思います。また、構造体の命名規則が一貫しており、タイトルケースの名前であれば、認識しやすくなります。
ポインタの型定義は、ユーザが const
でポインタを修飾することを排除してしまうので、特に悪質です。これは、他のルールで列挙されている理由から、大きな損失です。
関数ポインタの型定義は,関数ポインタを返す関数を宣言する必要があるときに 正当化されます.型定義なしの構文は耐え難いものです.また、関数ポインタの型が多くの場所で繰り返される場合(3つ以上、とか)にも typedef をします。すべての関数ポインタを型付けするのが好きな人もいますが、これでは何が起きていて何が期待されているのかがしばしば隠されてしまいます。関数ポインタの typedef が、実際にその型が何を表しているかを理解するのに役立つかどうか、注意深く考えてみてください。
enum に UPPERCASE_SNAKE
という名前をつけ、その値を小文字にする。
enum はほとんど単なる整数定数なので、 #define
d 定数と同じような名前を付けるのが自然です。enum
型のプレフィックスは enum の値を期待することを伝え、小文字の値のサフィックスはそれらが単なる整数定数ではないことを伝えることになります。
enum JSON_TYPE {
JSON_TYPE_null,
JSON_TYPE_boolean,
JSON_TYPE_number,
...
};
すべてのenumのサイズに対応する定数を定義する
ループ、配列、または enum
のビットフィールドを扱うための、汎用的で将来性のある方法は、そうでなければありません。常に列挙のサイズを示す定数を定義して、(あなたやあなたのユーザが) ハードコードされた値を避けるようにします。
enum SUIT {
SUIT_hearts,
SUIT_diamonds,
SUIT_clubs,
SUIT_spades
};
#define NUM_SUITS 4
私は、サイズを最後の enum 値にするのではなく、明示的に #define
するのが好きです。NUM_SUITSは聞いたことのないカードスーツです!また、前の enum 値のいずれかが明示的に設定されている場合 (たとえば
SUIT_hearts = 1` など)、最後の enum 値が enum のサイズを表さないことになるので、そのような事態も防ぐことができます。
名前を _
で始めたり、_t
で終わらせてはいけない: これらは標準のために予約されている
将来の ISO C 標準によって予約されている名前のリストです。types_like_this_t
と _anything
は将来の C 標準によって予約されている識別子であるため、あなた自身の識別子には使わないでください。
この種の名前は、どの型が言語標準の一部で、どの型がライブラリによって提供されるかを見分ける良い方法を提供することができた*はずです。残念ながら、it's not hard 人気のあるCライブラリやプロジェクトでこのような間違いをするものを見つけることは難しく、このようなルールの有用性は薄れてしまっているのです。
この間違いはあまりにも頻繁に起こります。あなたの図書館で同じ間違いを犯さないようにしましょう!
構造体のポインタは、nullity、動的配列、不完全な型にのみ使用します。
構造体のすべてのポインタは、セグメンテーションフォールトの機会である。
もし、ポインタがNULLであってはならず、未知のサイズの配列でもなく、構造体自身の型でもない場合は、ポインタにする必要はありません。構造体の中に、その構造体自身の型のメンバを入れればいいのです。構造体のサイズについては、ベンチマークを行うまで気にする必要はありません。
ポインタの引数は、Nullity、配列、修飾の場合のみ使用します。
このルールは、値がどこで変更されたかを読者が推論するのに役立ちます。また、 NULL
を受け取るべきでない関数が NULL
を受け取れないようにすることで、安全性も向上します。これは、参照渡しのセマンティクス (従って、ほとんどすべての場所で NULL
を有効な値として扱うこと) を要求する言語と比較して、大きな利点と言えます。
このルールに従ったコードベースを読んでいて、関数と型が最大限に分解されている場合、そのプロトタイプを読むだけで、関数が何をするのかがわかることがよくあります。これは、あらゆる場所でポインターを渡すプロジェクトとは対照的で、どこにも確信が持てません。
C言語では、構造体の値を関数に渡すと、pass-by-value semantics により、受け取った関数のスタックフレームにコピーされます。元の構造体は、その関数では変更できません(変更内容を返すことはできますが)。const` と同様に、この機能を可能な限り使用することで、読者があなたのプログラムについて推論することが容易になります。
ポインタメンバを持つ構造体を導入する場合、「修正」の定義が難しくなります。私は、構造体そのものや構造体のポインタに影響を与えるようなものを修正と呼んでいます。
構造体が関数によって「変更」される場合、その関数が構造体のポインタを受け入れる必要がなくても、受け入れるようにします。これにより、読者は、どの構造体がポインタメンバを持っているかを認識するために、関連する構造体の定義をすべて見つけて記憶する必要がなくなります。
typedef struct {
int population;
} State;
typedef struct {
State * states;
int num_states;
} Country;
// Good: `states` メンバが指す配列を `Country` 値だけで変更できる ** にもかかわらず、 `Country *` を受け取ります。
void country_grow( Country const * const country, double const percent ) {
for ( int i = 0; i < country->num_states; i += 1 ) {
country->states[ i ].population *= percent;
}
}
上の country
引数が const 型であることに注意してください。これは、国そのものを変更するのではなく、ポインタを変更することを意味しています (ポインタは null を表すともとれますが、関数名からはそうではなさそうです)。また、呼び出し元は Country const
へのポインタを渡すことができます。
ポインタの引数を使用する他の状況は、関数が有効な値として NULL
を受け入れる必要がある場合です (つまり、貧乏人の [Maybe] (http://learnyouahaskell.com/making-our-own-types-and-typeclasses)) 。その場合は、 const
を使用して、ポインタが変更されないようにシグナルを送り、 const
引数を受け取ることができるようにします。
// Good: `NULL` は空のリストを表し,list は const へのポインタである.
int list_length( List const * list ) {
int length = 0;
for ( ; list != NULL; list = list->next ) {
length += 1;
}
return length;
}
このルールに従うと、不完全な構造体型を捨てることになりますが、私はとにかく構造体型が好きではありません。(Cは[オブジェクト指向ではない](#c-isnt-object-oriented and you-shouldnt-pretend-it-is)" ルールを参照してください)。
ポインタを変更するよりも値を返すことを優先する
これは不変性を促進し、純粋関数を育成し、物事をより単純化し理解しやすくするものです。また、引数が NULL
である可能性を排除することで、安全性も向上している。
// 悪い点:不要な変異(たぶん)、安全でないこと
void drink_mix( Drink * const drink, Ingredient const ingr ) {
assert( drink != NULL );
color_blend( &( drink->color ), ingr.color );
drink->alcohol += ingr.alcohol;
}
// 良い点:不変性の石、どこでも純粋で安全な関数
Drink drink_mix( Drink const drink, Ingredient const ingr ) {
return ( Drink ){
.color = color_blend( drink.color, ingr.color ),
.alcohol = drink.alcohol + ingr.alcohol
};
}
これは必ずしもベストな方法とは言えませんが、常に検討すべきことです。
関数のオプション引数の名前に構造体を使用する
struct run_server_options {
char * port;
int backlog;
};
#define run_server( ... ) \
run_server_( ( struct run_server_options ){ \
/* default values */ \
.port = "45680", \
.backlog = 5, \
__VA_ARGS__ \
} )
int run_server_( struct run_server_options opts )
{
...
}
int main( void )
{
return run_server( .port = "3490", .backlog = 10 );
}
私はこれを21st Century Cで学びました。多くのCのインターフェイスは、このテクニックを利用すれば、非常に改善されるはずです。ソフトウェア開発において、(構文的な)名前付き引数の重要性と価値は、あまりにも見落とされがちです。もし納得がいかないなら、Bret Victor の Learnable Programming を読んでみてください。
名前付き引数をどこでも使ってはいけない。ある関数の唯一の引数がたまたま構造体であったとしても、それが必ずしもその関数の名前付き引数になるとは限りません。経験則から言うと、もしその構造体がその関数の外で使われるなら、上記のようなマクロで隠すべきでないということです。
// よかった。ここでのタイプ分けは参考になるし、期待できる。
book_new( ( Author ){ .name = "Dennis Ritchie" } );
構造体リテラルでは常に指定イニシャライザーを使用する。
// 悪い点 - 構造体のメンバを並べ替えると壊れるし、値が何を表しているのかが常に明確でない。
Fruit apple = { "red", "medium" };
// 良い; 将来性、描写力
Fruit watermelon = { .color = "green", .size = "large" };
名前付き引数の場合、このルールを曲げて、特定のフィールドを構造体の先頭に持ってきて、呼び出し側がその引数に名前を付けずに関数を呼び出せるようにすることがあります。
run_server( "3490" );
run_server( .port = "3490", .backlog = 10 );
もし、これを許可したいのであれば、明示的に文書化してください。フィールドの順序を変更した場合、ライブラリのバージョンを正しく変更するのはあなたの責任です。
構造体のメンバに対してのみアロケーションとフリー関数を提供する場合、構造体全体に対してメモリをアロケーションします。
もし、 foo_alloc
と foo_free
関数を提供して、 Foo
構造体のメンバに対してのみメモリを確保するのであれば、自動保存の利点と安全性を失うことになります。foo_allocと
foo_reeの関数は、
Foo` 構造体のメンバにメモリを割り当てるためだけに用意されているのであれば、自動保存の利点と安全性を失ってしまいます。
ゲッターとセッターを避ける
もし、あなたがC言語でカプセル化を求めているなら、おそらく物事を複雑にしすぎているのでしょう。構造体のメンバに直接アクセスしたり設定したりするようにしましょう。メンバの先頭に _
を付けてアクセスレベルを示すようなことはしないでください。構造体の不変量を宣言しておけば、ユーザーが何かを壊してしまうことを心配する必要はありません。
[別のルール](#always-prefer-to-return-a-value-rather than modifying-pointers) でアドバイスしたように、可能な限り変異を避けるようにしましょう。
// というより、むしろ
void city_set_state( City * const c, char const * const state )
{
c->state = state;
c->country = country_of_state( state );
}
// 常に不変と純粋を好む。
City city_with_state( City c, char const * const state )
{
c.state = state;
c.country = country_of_state( state );
return c;
}
City c = { .name = "Vancouver" };
c = city_with_state( c, "BC" );
printf( "%s is in %s, did you know?\n", c.name, c.country );
しかし、常に宣言的プログラミングを可能にするインターフェイスを提供する必要があります。
City const c = city_new( .name = "Boston", .state = "MA" );
printf( "I think I'm going to %s,\n"
"Where no one changes my state\n", c.name, c.country );
Cはオブジェクト指向ではないし、オブジェクト指向のふりをするべきではない
C言語にはクラスもメソッドも継承もオブジェクトのカプセル化も真のポリモーフィズムもないのです。失礼かもしれませんが、でも失礼な言い方ですが、deal with it です。C言語では、それらのくだらない、複雑な模倣を実現できるかもしれませんが、その価値はないのです。
結局のところ、C言語にはすでに完全に対応できる言語モデルがあるのです。C言語では、データ構造を定義し、そのデータ構造を組み合わせて使う機能を定義する。データと機能が複雑な仕掛けで絡み合っていない、これは良いことです。
言語設計の最先端を行くHaskellも、同じようにデータと機能を切り離す決断をしたのです。Haskellを学ぶことは、プログラマーが自分の技術を向上させるためにできる最善のことのひとつですが、CとHaskellの根本的な共通点から、Cプログラマーには特に有益だと思います。確かにC言語には無名関数がありませんし、C言語ですぐにモナドを書くこともないでしょう。しかし、Haskellを学ぶことで、クラスなし、ミュータビリティなし、モジュール性ありの優れたソフトウェアを書く方法を学ぶことができます。これらの性質は、優れたC言語プログラミングにとって非常に有益なものです。
他のパラダイムをCに移植しようとするのではなく、Cが提供するものを受け入れ、評価しましょう。