第5回 int 型のサイズ

C言語の整数型は処理系によってサイズが異なります。標準規格では、それぞれの整数型が少なくともどれだけの表現範囲を持っているか、そして、それぞれの整数型の間の表現範囲の大小関係だけが決められています。

整数型の中でも、int 型のサイズは 16 ビットと 32 ビットの処理系がそれなりに多く存在することもあり、入門書や解説書でも注意が促されることが多いようです。最近では long 型が 64 ビットの処理系もありますので、整数型のサイズを取り巻く状況はもう少し複雑になってきています。

そうした中、極力ソースコードの移植性を高めようということで、int 型のようにサイズがよくわからない型は使用せず、int16 型とか、int32 型のような型を定義して、「それらを使うべし」とするコーディング規約もよく見かけます。それで本当に、int 型のサイズに関する移植性の問題は解消されたのでしょうか?

残念ながら、世の中はそう甘くありません。どんな型定義を行おうとも、C言語に関わっている以上、int 型のサイズにまつわる問題は地の果てまで追いかけてきます。今回は、そうした int 型のサイズに関する解説です。

汎整数拡張

汎整数拡張(integral promotion)という用語を見聞きされたことがあるでしょうか? この用語は、同じ意味にもかかわらず、いろいろ異なる表記をされることがあります。それも標準規格の中でです。

例えば、C++規格である JIS X3014:2003 の中では、「汎整数昇格」という訳語が使われています。また、C99では「整数拡張(integer promotion)」という用語になぜか変わっています。すべて同じ意味ですが、ここでは「汎整数拡張」という表記に統一したいと思います。

汎整数拡張というのは一体何でしょうか? ごく簡単にいうと、何らかの演算を行うときには、オペランドの値が int 型で表現できる場合は int 型に、unsigned int 型で表現できる場合は unsigned int 型に、暗黙的に型変換が行われることです。「オペランドの値が~」と書きましたが、これは何も、実行時に実際にどんな値なのかを調べるわけではなく、あくまでもオペランドの型だけを頼りに、静的に判断されます。

例えば、次のような処理系を考えてみましょう。

表現範囲
char -128~+127
short -32768~+32767
int -2147483648~+2147483647
long -2147483648~+2147483647

近年では最も典型的な処理系ですが、この場合、汎整数拡張によって、char 型と short 型は int 型に型変換されることになります。また、unsigned char 型と unsigned short 型も、その表現範囲全体が int 型でも表現できますから、int 型に型変換されます。

つまり、元が符号無し整数型であっても、知らないうちに勝手に int 型に変換される可能性があるわけです。これは次のような状況で、勘違いを生み出す原因になります。

unsigned char c = 0x10;
if (c - 0x20 <= 0x5e)
{
  ...
}

上記のコードは、ASCII 文字が 0x20~0x7e の範囲に収まっているかどうかを判定しようとしているようです。素直に書くと、

if (0x20 <= c && c <= 0x7e)
{
  ...
}

ですが、比較を 2 回行う必要があるので、少しでも最適化しようとしたのでしょう。しかし、上のコードは期待通りには動いてくれません。というのも、上のコードでは、

c - 0x20

において、cunsigned char 型なので、0x20 を引いても負にはならず、0xf0 になることを期待しているわけですが、実際には、減算の前に汎整数拡張*1が発生しますので、int 型として演算を行うことになります。結果として、c - 0x20 は -0x10 になりますので、期待は完全に裏切られます。

他の例を挙げてみましょう。

char a = 0;
if (sizeof(+a) == sizeof(a))
{
  ...
}

ちょっとわざとらしい例ですが、上のコードでは、if の条件式は真になるでしょうか?それとも偽になるでしょうか?

結果は偽になります。単項の+演算子というのは、何も行わない演算子だと理解されていることが多いと思います。しかし、この演算子のオペランドも汎整数拡張が行われます。それに対して、sizeof 演算子のオペランドは汎整数拡張が行われません。結果として、左辺は int 型であり、右辺は char 型になりますので、今回仮定している処理系では両辺は等しくなりません。

このように、例えどんなに型定義などを使って int 型のサイズを隠蔽したとしても、何らかの演算を行うと、int 型のサイズの影響が露骨に現れることになります。


*1 より厳密には、通常の算術型変換の過程として汎整数拡張が起きます。

整数定数

整数定数(いわゆるリテラル)にも int 型のサイズが関係してきます。整数定数の型は、(ちょっと複雑ですが)次の手順で決定されます。

  1. int 型の表現範囲であれば int
  2. int 型の表現範囲になく、unsigned int 型の表現範囲にある 8 進または 16 進数の場合は unsigned int
  3. long 型の表現範囲であれば long
  4. そうでなければ、unsigned long

ただし、C99の場合には、long long 型が存在しますので、long 型の表現範囲にない 10 進定数は long long 型になります(以下省略)。

このように、整数定数の型は、その値によって決まります。すなわち、int 型のサイズによって、整数定数がどんな型になるかが変わるわけです。特に、0xffff のような整数定数は、unsigned int 型になったり int 型になったりしますので要注意です。

ここで、整数定数の型に関するありがちな勘違いの具体例を挙げてみます。なお、今度は、先ほどとは違って int 型が 16 ビットで、その表現範囲が -32758~+32767 の処理系について考えてみることにします。

typedef int count_t;
#define COUNT_MAX  32767
#define COUNT_MIN  -32768

上のコードのおかしな点に気付くでしょうか?

おかしな点は COUNT_MIN マクロの定義にあります。int 型の表現範囲は -32768~+32767 なので、int 型に定義されている count_t の最小値を表す COUNT_MIN マクロが(-32768)なのは当然のような気がします。

しかし、値はともかく、問題は COUNT_MIN の型にあります。32768 というのは、int 型(表現範囲は -32768~+32767 と仮定)の表現範囲に収まりません。また、8 進定数でも 16 進定数でもありませんから、32768 の型は long 型です。long 型のオペランドに単項の - 演算子を付けても、やはり long 型です。

つまり、上のコードのような COUNT_MIN マクロの定義では、本来 int 型の定数式に展開されるべきであるにも関わらず、long 型になってしまうわけです。では、どのように定義すれば int 型になるのでしょうか?

それは、次のようにします。

#define COUNT_MIN  (-32767-1)

今度は、-32767 から 1 を引いています。32767 は int 型の表現範囲に収まっていますから int 型です。そして、それに単項の - 演算子を付けてもやはり int 型です。また、int 型である 1 との減算を行っても、結果はやはり int 型になります。

いかがでしょうか? int 型のサイズを隠蔽するために別の型を定義するコーディング規約は多いのですが、それをやってしまうと、一見しただけでは式や定数の振る舞いが分からなくなってしまいます。なぜなら、定義された型の本当の型が分からないからです。

今回は主に int 型に焦点を当てましたが、整数型のサイズに関しては、まだ他にもいろいろな注意点があります。それらについては、また別の機会にお話したいと思います。