C言語は最初の 27 年間を真偽型なしで過ごしました。これは見落としではなく、初日から出会うことになる結果を伴う設計上の判断であり、そこから直接生まれる有名なバグが 1 つあります。
ゼロが偽、それ以外はすべて真
C言語の条件の規則に、特別な真/偽の値はありません。if、while、for、&&、||、! はすべて、たった 1 つのことを問います。この値はゼロか?
負の数も真です。小数も真です。ヌルでないポインタも真です。偽なのは、どんな型であれゼロだけです。
だから C言語のコードは、この規則を知るまでは素っ気なく見えるイディオムであふれています:
if (count) /* count がゼロでないなら */
if (!count) /* count がゼロなら */
if (p) /* p が NULL でないなら */
if (!strcmp(a, b)) /* 文字列が等しいなら - strcmp は一致で 0 を返す */
最後のものは人をつまずかせます。strcmp は文字列が一致したとき 0 を返すので、「等しい」が if の中では「〜でない」と読めてしまうのです。if (strcmp(a, b) == 0) と書けば、同じことをより明確に表せます。
比較は int を生む
C言語の比較は、特別な種類の式ではありません。値が 1 か 0 である普通の int に評価されるので、表示したり、保存したり、算術に使ったりできます:
! 演算子も同じように働きます。オペランドがゼロなら 1 を、それ以外なら 0 を生みます。だから !!x は、任意の値をちょうど 0 か 1 に正規化する古典的なイディオムです。
bool、true、false(C99)
C99 は _Bool という本物の真偽型と、それに読みやすい名前を与えるヘッダーを追加しました:
<stdbool.h> はごく小さなヘッダーです。bool を _Bool として、true を 1 として、false を 0 として定義するだけ。それがヘッダーのすべてです。それでも、これを使えば int flag = 1; では決して伝わらない意図が目に見えるようになります。
_Bool には本当に他と違う挙動が 1 つあります。0 か 1 しか格納しないことです。ゼロ以外の値を代入すると、ちょうど 1 に変換されます:
printf には今でも真偽値用の %b はありません。%d で表示するか、自分で文字列に変換しましょう:
C23 ではさらに簡単になりました。bool、true、false が本物のキーワードになったので、インクルードはもう必要ありません。とはいえ今日のコードの多くはまだ C17 を対象にしているので、#include <stdbool.h> は残しておきましょう。
関数から真偽値を返す
bool の価値が最もはっきり分かるのはここです:
戻り値の型が bool なら、その値が何を意味するのかを呼び出し側に伝えられます。check_file という名前の関数が int を返すとしたら、それは真偽値かもしれないし、個数かもしれないし、エラーコードかもしれません。そして C言語のライブラリの慣習では 3 番目であることが多く、そこでは 0 が成功を意味します。型に名前を付ければ、その当てずっぽうはなくなります。
= と == のバグ
代入は代入した値を生む式であり、しかもゼロ以外の値はすべて真なので、これはコンパイルが通ってしまいます:
一度に 2 つのことが壊れました。条件式が x に 5 を代入して元の値を破壊し、そのうえで 5 を判定し、それが非ゼロなので分岐が実行されたのです。プログラムにはエラーもなく、既定では警告も出ません。
さらに悪いことに、0 を使った版は分岐が決して実行されません:
if (found = 0) { /* never taken - and found is now 0 */ }
守り方は 3 つ、役に立つ順に挙げます。
警告を有効にしてコンパイルする。 -Wall が捕まえてくれます:
warning: suggest parentheses around assignment used as truth value
警告をゼロにすることが本当の解決策であり、コストはフラグ 1 つです。
定数を先に書く。 「ヨーダ記法」は、この打ち間違いをコンパイルエラーに変えます:
if (5 == x) /* correct */
if (5 = x) /* error: not an lvalue - the compiler stops you */
これを好むチームもあれば、読みにくいと感じるチームもあります。どちらの立場にも言い分がありますが、確かに効果はあります。
意図してやるときは、意図が分かるように書く。 条件の中での代入は、入力を読むときには本当に役立ちます:
int c;
while ((c = getchar()) != EOF) { ... }
c = getchar() を囲む余分な括弧は優先順位のために必要ですが、同時にその代入が意図的であることも示しています。これがイディオムです。あなたの条件内代入がこの形をしていないなら、それはおそらく打ち間違いです。
よくある真偽値の間違い
true と比較する。 if (flag == true) は stdbool.h があれば動きますが、flag が 42 を持つ int になった瞬間に壊れます。42 == 1 は偽だからです。if (flag) と書きましょう。
比較を連鎖させる。 if (0 < x < 10) は常に真です。0 < x は 0 か 1 を生み、どちらも 10 より小さいからです。if (x > 0 && x < 10) と書きましょう。
論理演算子のつもりでビット演算子を使う。 & と | は && と || ではありません。短絡評価をせず、ビット単位で組み合わせます:
どちらのオペランドも「真」なのに、a & b は 0 です。&& のつもりで & を使うと、特定の値のときだけ間違う条件ができあがります。これは最も見つけにくい種類のバグです。
浮動小数点数を等価比較する。 if (0.1 + 0.2 == 0.3) は偽です。代わりに許容誤差と比べましょう。理由はデータ型を参照してください。
よくある質問
C言語に真偽型はありますか?
C99 以降はあります。_Bool が組み込みの型で、<stdbool.h> をインクルードすれば bool、true、false という読みやすい書き方が使えます。C99 より前には真偽型がまったくなく、プログラムは int に偽を 0、真を 1 として持たせていました。これは今でも動きますし、今でもよく見かけます。
C言語では何が真とみなされますか?
ゼロ以外のあらゆる値です。条件式では 1、-5、0.01、そしてヌルでないポインタはすべて真であり、偽なのは 0、0.0、ヌルポインタだけです。真偽値専用のチェックは存在せず、if (x) は単に x がゼロでないかを調べているだけです。
C言語で bool を使うには?
#include <stdbool.h> を加えれば、bool ready = true; が期待どおりに動きます。内部では bool は _Bool であり、これは 0 か 1 しか格納しません。ゼロ以外の値を代入すると、ちょうど 1 が格納されます。
C言語の = と == の違いは何ですか?
= は代入、== は比較です。if (x = 5) は x に 5 を代入してから 5 を判定し、5 は非ゼロなので分岐は必ず実行されます。if (x == 5) は等価性を調べます。前者はエラーなくコンパイルされてしまいます。-Wall も、if (5 == x) と書く習慣も、そのために存在します。