汎整数拡張

 何かプログラムの話題を書かないとロベールの小部屋らしくないので、とりあえず適当にマニアックな話題でも。

 さて、次の C のプログラムを実行すると何と出力されるだろうか。

#include <stdio.h>

int main() {
    unsigned char a = 1, b = 2;
    printf("%d\n", a - b);
    return 0;
}

 a も b も unsigned char だから、a - b はアンダーフローして(CHAR_BIT が 8 なら)(unsigned char)255 になり、それが printf に渡される時に暗黙の型変換により (int)255 に拡張されて、結局 255 が出力されると思うかもしれない。ところが実際にやってみると、この結果は -1 になる。
 これは、ANSI C の汎整数拡張という仕様によるものである(C99 では整数拡張と、C++ では汎整数昇格と呼ぶらしい)。実は、整数が演算中に出てきた場合、int より小さいとその時点で int に拡張されてしまうのだ。従って、(int)a - (int)b と書いたのと同じ事になり、1 - 2 = -1 になるわけである。
 細かい話をすると、K&R C の時代では unsigned char なら符号属性を保持して unsigned int に拡張されたが、ANSI C になると unsigned char の値は全て int で表現できるということで、int に拡張されるようになったそうだ。ただし、例えば short と int が両方とも 2 バイトの場合は、unsigned short は int では表現しきれないため、unsigned int に拡張(昇格?)される。
 これを回避するためにと次のようにキャストしても無駄である。

    printf("%d\n", (unsigned char)a - (unsigned char)b);

なぜなら、これは

    printf("%d\n", (int)(unsigned char)a - (int)(unsigned char)b);

と同じ事になるからである。ちゃんと回避したければ、

    printf("%d\n", (unsigned char)(a - b));

と書く必要がある。これでちゃんと 255 と出力されるようになる。
 このあたりを把握してない事で何かバグが出るのかどうかは知らないが(多分、ちょっとテストすればすぐ気づく)、#if と typedef を使って環境毎に型をこちょこちょ切り替えてる場合、テストをサボってると思わぬ所でバグが出る可能性があるかもしれない。まあ、とりあえず C/C++ の仕様の中ではややこしい部類に入ると思うことだけは確かだ。