C言語の整数型は処理系によってサイズが異なります。標準規格では、それぞれの整数型が少なくともどれだけの表現範囲を持っているか、そして、それぞれの整数型の間の表現範囲の大小関係だけが決められています。
整数型の中でも、int型のサイズは16ビットと32ビットの処理系がそれなりに多く存在することもあり、入門書や解説書でも注意が促されることが多いようです。最近では long型が64ビットの処理系も普通にありますので、整数型のサイズを取り巻く状況はもう少し複雑になってきています。
そうした中、極力ソースコードの移植性を高めようということで、int型のようにサイズがよくわからない型は使用せず、int16型やint32型のような型を定義して、「それらを使うべし」とするコーディング規約もよく見かけます。それで本当にint型のサイズに関する移植性の問題は解消されたのでしょうか?
残念ながら、世の中はそう甘くありません。どんな型定義を行おうとも、C言語に関わっている以上、int型のサイズにまつわる問題は地の果てまで追いかけてきます。今回は、そうしたint型のサイズに関する解説です。
整数拡張
整数拡張(integer promotion)という用語を見聞きされたことがあるでしょうか? この用語は、同じ意味にもかかわらず、いろいろ異なる表記をされることがあります。それも標準規格の中でです。
例えば、C99より前のC規格では「汎整数拡張(integral promotion)」という用語が使われていました。C++規格でも現在に至るまでintegral promotionで、JIS X3014:2003の中では「汎整数昇格」という訳語が使われています。すべて同じ意味ですが、ここでは「整数拡張」という表記に統一したいと思います。
整数拡張というのは一体何でしょうか? ごく簡単にいうと、何らかの演算を行うときには、オペランドの値がint型で表現できる場合はint型に、unsigned int型で表現できる場合はunsigned int型に、暗黙的に型変換が行われることです。「オペランドの値が~」と書きましたが、これは何も、実行時に実際にどんな値なのかを調べるのではなく、あくまでもオペランドの型だけを頼りに、静的に判断されます。
例えば、次のような処理系を考えてみましょう。
型 | 表現範囲 |
---|---|
char | -128~+127 |
short | -32768~+32767 |
int | -2147483648~+2147483647 |
long | -2147483648~+2147483647 |
long long | -9223372036854775808~+9223372036854775807 |
近年ではよく見かける処理系ですが、この場合、整数拡張によってchar型とshort型はint型に型変換されることになります。また、unsigned char型とunsigned short型も、その表現範囲全体がint型でも表現できますから、int型に型変換されます。
つまり、元が符号無し整数型であっても、知らないうちに勝手にint型に変換される可能性があるわけです。これは次のような状況で、勘違いを生み出す原因になります。
0 1 2 3 4 5 6 |
unsigned char c = 0x10; if (c - 0x20 <= 0x5e) { ... } |
上記のコードは、文字が0x20~0x7eの範囲に収まっているかどうかを判定しようとしているようです。素直に書くと、
0 1 2 3 4 5 |
if (0x20 <= c && c <= 0x7e) { ... } |
ですが、比較を2回行う必要があるので、少しでも最適化しようとしたのでしょう。しかし、上のコードは期待通りには動いてくれません。というのも、上のコードでは、
0 1 2 |
c - 0x20 |
において、cがunsigned char型なので、0x20を引いても負にはならず、0xf0になることを期待しているわけですが、実際には、減算の前に整数拡張*1が発生しますので、int型として演算を行うことになります。結果として、c – 0x20は-0x10になりますので、期待は完全に裏切られます。
他の例を挙げてみましょう。
0 1 2 3 4 5 6 |
char a = 0; if (sizeof(+a) == sizeof(a)) { ... } |
ちょっとわざとらしい例ですが、上のコードでは、if の条件式は真になるでしょうか?それとも偽になるでしょうか?
結果は偽になります。単項の+演算子というのは、何も行わない演算子だと理解されていることが多いと思います。しかし、この演算子のオペランドも整数拡張が行われます。それに対して、sizeof演算子のオペランドは整数拡張が行われません。結果として、左辺はint型であり、右辺はchar型になりますので、今回仮定している処理系では両辺は等しくなりません。
このように、例えどんなに型定義などを使ってint型のサイズを隠蔽したとしても、何らかの演算を行うと、int型のサイズの影響が露骨に現れることになります。
整数定数
整数定数(123など)にもint型のサイズが関係してきます。整数定数の型は、(ちょっと複雑ですが)次の手順で決定されます。
- int型の表現範囲であればint型
- int型の表現範囲になく、unsigned int型の表現範囲にある8進または 16進数の場合はunsigned int型
- long型の表現範囲であればlong型
- long型の表現範囲になく、unsigned long型の表現範囲にある8進または 16進数の場合はunsigned long型
- long long型の表現範囲であればlong long型
- そうでなければ、unsigned long long型
ただし、C99より前の規格では、long long型が存在しませんので、long型の表現範囲になければunaigned long型になります。
このように、整数定数の型はその値によって決まります。すなわち、int型のサイズによって、整数定数がどんな型になるかが変わってきます。特に、0xffffのような整数定数は、unsigned int型になったりint型になったりしますので要注意です。
ここで、整数定数の型に関するありがちな勘違いの具体例を挙げてみます。なお、今回はint型が16ビットで、その表現範囲が-32758~+32767の処理系について考えてみることにします。
0 1 2 3 4 |
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型になるのでしょうか?
正しくは、次のようにします。
0 1 2 |
#define COUNT_MIN (-32767-1) |
今度は、-32767から1を引いています。32767はint型の表現範囲に収まっていますからint型です。そして、それに単項の-演算子を付けてもやはりint型です。また、int型である1を引いても、結果はやはりint型になります。
いかがでしょうか? int型のサイズを隠蔽するために別の型を定義するコーディング規約は多いのですが、それをやってしまうと、一見しただけでは式や定数の振る舞いが分からなくなってしまいます。なぜなら、定義された型の本当の型が分からないからです。
今回は主にint型に焦点を当てましたが、整数型のサイズに関しては、まだ他にもいろいろな注意点があります。それらについては、また別の機会にお話したいと思います。