【汎整数拡張】第4回 (2^n)による剰余とビットマスク(2^n)-1によるAND演算に等価性はあるか

整数型の値に対して、その下位nビットを取り出す場合、

x & ((1<<n)-1)

と書いたりしますが、これって2を法とする剰余(2^nで割った余り)でも表現できないか?と考えたことはあると思います。

まず、char/unsigned char型変数の値127に対して、(2^n)-1でマスクをかけてみます。

> char : 127
0111 1111
> char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> c & 0x0F
0000 0000 0000 0000 0000 0000 0000 1111

> unsigned char : 127
0111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> uc & 0x0F
0000 0000 0000 0000 0000 0000 0000 1111

と、確かに下位4ビット以外のビットがゼロクリアされています。
(途中でchar型の値が32ビットで表示されていますが、char型が式として評価されることによる汎整数拡張を示しています)

では、最上位ビットが1である場合はどうでしょうか。
char型の-1、unsigned char型の255に対して、同じく(2^n)-1でマスクをかけてみます。

> char : -1
1111 1111
> char -> int
1111 1111 1111 1111 1111 1111 1111 1111
> c & 0x0000000F
0000 0000 0000 0000 0000 0000 0000 1111

> unsigned char : 128
1111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 1111 1111
> uc & 0x0000000F
0000 0000 0000 0000 0000 0000 0000 1111

当然の結果ですが、確かに下位4ビット以外のビットがゼロクリアされています。

つぎに、「剰余演算子を使って下位nビットのビットマスクを再現する」ことを実験してみます。
まず、最上位ビット(符号ビット)が0の場合。

> char : 127
0111 1111
> char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> c % 0x00000010
0000 0000 0000 0000 0000 0000 0000 1111

> unsigned char : 127
0111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> uc % 0x00000010
0000 0000 0000 0000 0000 0000 0000 1111

確かに、下位4ビット以外のビットがゼロクリアされています。
ここまでは、ビットマスクと剰余演算子とには等価性があると言えそうです。

では、最上位ビット(符号ビット)が1の場合。

> char : -1
1111 1111
> char -> int
1111 1111 1111 1111 1111 1111 1111 1111 …(1)
> c % 0x00000010
1111 1111 1111 1111 1111 1111 1111 1111 …(2)

> unsigned char : 255
1111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 1111 1111
> uc % 0x00000010
0000 0000 0000 0000 0000 0000 0000 1111

元のデータ型がunsigned char型の場合は、想定した通りに下位4ビット以外のビットがゼロクリアされていますが、char型の場合(1)→(2)の演算では変化が起きていません。

これは言う間でもないですが…
まず、式の中でchar型変数の値が汎整数拡張によりint型に型昇格した時点で、最上位の符号ビットは1となります(1)。char型→int型の型昇格においては、空きビットには符号ビットと同じビットがセットされます。

そして、(1)の値に対して0x00000010で剰余を求める(2)のですが、まず、2^nによる剰余を求める演算とは何か、について考えてみます。

2^nによる剰余を求める演算というのは、ビット表現で言うと「下位nビット以外のすべての上位ビットを符号ビットでクリアする」ということになります。なので、(1)の値は符号ビットが1ですが、5ビット目以上のすべてのビットはもともと「1」であるため、符号ビット「1」でクリアしても、その剰余は(1)の値から変化しないことになります。

試しに、-118(0x8A)で実験してみます。

> char : 0x8A
1000 1010
> char -> int
1111 1111 1111 1111 1111 1111 1000 1010
> c % 0x00000010
1111 1111 1111 1111 1111 1111 1111 1010

というように、下位4ビット(1010)が保存され、より上位のすべてのビットが、元の符号ビットと同じ「1」でクリアされています。

まとめると、

  1. signed型の値xに対する2^nによる剰余は、下位nビットを保存し、より上位のすべてのビットをxの符号ビットでクリアする。
  2. unsigned型の値xに対する2^nによる剰余は、下位nビットを保存し、より上位のすべてのビットを「0」でクリアする。これは、ビットマスク(2^n)-1によるAND演算と等価である。

と、難しく考えましたが、-118/16=7余り-6、であることからすると当然なんですけどね(0xFAを8ビットの2の補数表現として解釈すると、-6)。