C言語 _Bool
C言語の_Bool型はC++のbool型と同等なのか
1972年に登場したC言語には、真偽値を格納するための「Boolean型」が27年間存在しなかった。
なのでほとんどのプログラマーは、int型の変数に「0 = false / 0以外 = true」として真偽値を格納していた。
intや数字だと分かりにくいので、大抵は下のようにtypedefやdefineして読みやすくしていたものの、中身はintなので当初は2バイト、32bitの時代になると4バイト消費していたわけだ。
typedef int bool;
#define true 1
#define false 0
上の例では小文字で書いているが、実際は「BOOL」「TRUE」などと全大文字で書いていたプログラマーが多い。(標準Cでそういう決まりがあるわけではないが)
実際、WindowsのAPI周りもintをtypedefした「WINBOOL」、さらにそれをdefineした「BOOL」、1と0をdefineした「TRUE」「FALSE」があちこちに見受けられる。
C言語を拡張して1983年に登場したC++にはbool型があり、これをsizeofで調べると1バイトと表示される。
C言語を拡張することなくオブジェクト指向型言語へシフトして1984年に出てきたObjective-Cでは、下のようにsigned char型をtypedefした「BOOL」が使用されている。
typedef signed char BOOL;
#define YES (BOOL)1
#define NO (BOOL)0
なのでObjective-Cにおいても真偽値を格納する変数はC++と同じく1バイトだ。
余談だがObjective-Cでは「YES / NO」で真偽値を記述する。(現在は_Bool型も使える)
C言語もsigned charに格納すれば1バイトで済む(signed charの0も真偽判定をすれば偽になる)のだが、intに格納することがすっかり慣例化してしまい、長らく真偽値格納のために4バイトも食う状態が続いていた。
この原因は「charは文字型であり数値を収める型ではない」という設計思想が根幹にあったことや、C言語においての比較演算結果自体の型がintだったことが大きいように思う。
例えば「a == b」という比較の結果はint型の0か1で得られるので、それをsigned charに格納するには明示的・暗黙的関わらずキャストをする必要があるわけで、それが「C的に美しくない」ということだったのかもしれない。
そんな中1999年に発行されたC99で、C言語にも待望のBoolean型である「_Bool」が追加された。
本当ならC++と同じ型名の「bool」を追加したかったのだろうが、この時点ですでに多くのCプログラム上にdefineやtypedefされた「bool」が散在していたし、「bool」という名の変数や関数を作っているコードもあったため、それらのプログラムがビルドできなくなってしまうリスクがあって追加できなかったようだ。(同じような理由で64bit値の型名も別名で追加されることなく「long」を2つ繋げただけの「long long」となった)
一度広く普及した言語に対して、後から予約語を増やすというのは不可能に近いほど難しい。
そこで標準Cでは予め「予約済み識別子」という、プログラムを記述する際の決め事を作っていた。
これは世界中のCプログラマーに「将来予約語になる可能性があるから、このような記述は避けるようにしてね」と周知するためのものだ。
予約済み識別子にはアンダースコア+英大文字から始まるもの(「_Abcd」など)と、アンダースコア2連続から始まるもの(「__abcd」など)、それにグローバルスコープを持ちつつアンダースコアから始まるもの(「_abcd」という広域変数など)がある。
もちろんコンパイラ側で制限がかけられているものではないので、この決まりを守らない(というか予約済み識別子自体を知らない)プログラマーもたくさんいて、そういう人が書いたコードはある日突然ビルドが通らなくなる日が来る可能性があるわけだが、現在でも標準Cではそういう決まりになっている。
つまり「_Bool」はこの「予約済み識別子」を使って追加されている。
取って付けたような型名になっているのはそのためだ。
ただ、予約語として追加されたのは「_Bool」という型名だけであり、「_True」や「_False」といった値名は追加されていない。
なので_Bool型がとれる値は、数値の「1」と「0」になる。
ここがC++とはまったく違うところだ。
さすがにこれでは使いにくいと思ったのか、stdbool.hをインクルードすると「true」「false」が使えるようになり、型名も「bool」が使える。
とはいえこの中身は下のようにただdefineしているだけだ。
#define bool _Bool
#define true 1
#define false 0
このような実装から「なんだよただのマクロじゃん、中身は今までと同じintなんじゃないか」と思われがちだが、そうでもない。
#include <stdio.h>
int main(void){
_Bool b;
printf("%d\n", sizeof b); // => 1
b = 0;
printf("%d\n", b); // => 0
b = 1;
printf("%d\n", b); // => 1
b = 256;
printf("%d\n", b); // => 1
b = -1;
printf("%d\n", b); // => 1
b = 1;
int a = 10 + b;
printf("%d\n", a); // => 11
}
Ubuntu上のclangとWindows上のMinGW-w64のCコンパイラでの検証結果だが、_Boolのサイズは1バイトであり、0以外の数値を代入しようとしてもすべて1に丸め込まれる。
intなどの数値型に対して加減算もできる。(もちろん暗黙的キャストが行われた結果だが)
これは同じclangやMinGW-w64のC++コンパイラで、bool型相手に行ったときとまったく同じサイズと挙動だ。
ちなみに_Bool型のサイズに関しては特に決められておらず環境依存なので、4バイト消費するコンパイラはあるかもしれないが、とれる値は0か1のどちらかだと決められている。
ただし上でも少し触れたが、以下のようなコードの実行結果はCとC++で異なる。
printf("%d\n", sizeof(0 == 0));
上記と同環境のC++だと1バイトだが、Cだとsizeof(int)と同じサイズになる。(現代だと大抵4バイト)
値同士の比較の結果は、C++はbool型だがCはint型の0か1だ。
_Bool b = (0 == 0);
上のようなコードはintの1が_Boolへ暗黙的にキャストされた結果、_Bool型の1が変数bに入るわけだ。
この挙動は一応頭に置いた上でコーディングしないと、_Generic絡みなどで思わぬバグを生むことがある。
ちなみにC++だと
auto b = (0 == 0);
と書けば、変数bにbool型のtrueが入る。
これは生まれながらにして真偽型を持っているC++と、後から拡張されて真偽型が追加されたCの違いだ。
Cのこの挙動を今になってC++に合わせてしまうと、過去の膨大な数のCプログラムが正常に動かなくなってしまうリスクがあるので、従来どおりのサイズ(int)で据え置かれている。
ただしあくまでこれは比較演算結果の型であって、真偽型を格納するための箱(または関数の戻り値)としての型ではない。
_Boolは後者のために追加された型なので、真偽値を格納する変数や関数の戻り値の型として使用する範囲であればC++のboolと「同等」と見て良いのではないかと思う。(言語の仕様拡張はややこしい)
余談。
C23では「bool」「true」「false」が定義済みになるようだ。
つまりincludeしなくても、_Boolではない「bool」が型名として使え、真偽値も「true」と「false」が使えるようになるらしい。(ということは真偽型が_Boolとboolの2つになるということ?)
あと「nullptr」も定義済みとして使えるようになるらしい。
C99から24年が経ち、ついに予約済み識別子ではない領域が使われる時代になった。
ただ「予約済み」ではなく「定義済み」なので、これらは予約語に追加されるわけではなく、Goの「true」「false」のような定義済み識別子となるのかもしれない。(ちなみにC99で追加された「inline」は記述箇所が限定されているためか予約済み識別子は使われていない)
完全に余談。
C言語では、char, signed / unsigned char, signed / unsigned short, signed / unsigned int, signed / unsigned long, signed / unsigned long long, float, double, long double の13種類すべての型で真偽判定すると、値が0または0.0ならば偽、0(0.0)以外であれば真となる。
|