char同士の演算もintで行われる

次のコード(C言語)を実行すると、どのように出力されるだろうか。

#include <stdio.h>

int main(){
    unsigned char a = 0, b = ~a;
    if (b == ~a){
        printf("b == ~a\n");
    } else {
        printf("b != ~a\n");
    }
    printf("%d\n", ~a);
    printf("%d\n", b);
    return 0;
}

実際に出力させてみると、次のようになる。
たった今bに~aを代入したばかりなのに、b != ~aだとは!

b != ~a
-1
255

n[255]を意図してn[~t](unsigned char t = 0;)と書いたのにそうならず、はまった。
これはn[-1](-1は0xffffffff)という意味になってしまう。

unsigned charなんだから、「~」演算子アセンブラレベルなら8bit幅でnot blと
しているに決まっていると思い込んでいたのだが、
実際にアセンブリコードを吐かせてみると、32bitでnot ebxとされていた。

n[(unsigned char)~t]と書けばn[255]になるということはすぐ気づいいたが、
なぜそういう挙動なのかがわからず悩んだ。
だいいち、なぜ32bitなのか、その必然性がわからない。

汎整数拡張 - ロベールの小部屋
色々調べた結果、↑のページを発見。
printf("%d\n", ~a);で-1と表示されるような現象には割とよく出会うので、
「%dは32bit整数か何かで、それに合わせて変換されるのだろう」などと
漠然と思っていたが、的外れな理解だった。
(そもそも、それだとprintf("%d\n", b);と出力が異なることに説明がつかない)

unsigned char t = 0 に対し、~tとした時点で既にint型の-1に変換されているのだ。
これを再びunsigned charへキャストすれば255になるから、なかなか普段は気づかない。
32bitである必然性はintのサイズだった。
それにしても、ビット反転させるだけの演算でも(ていうかどんな演算でも)、
符号なしのunsigned charを符号付きのintに変換してから行う仕様だとは。
知識がないというのは怖い。

C#でも同様のコードを試してみたら、b = ~aの代入でエラーになった。
C#でも~aはintに変換されているが、暗黙的にはbyteへ戻せない。
バイト単位でビット反転させたいだけでも、明示的にキャストする必要があるのだ。
また、n[~(byte)0]がn[-1]の意味になる挙動もCと同じ。
キャストや配列の範囲チェックが厳しいので、Cよりはだいぶ間違いに気づきやすいが、
intへ変換してから計算するという仕様は一緒なのだね。

ちなみにVB6.0では、aがByte型の0のときNot aは255。