汎整数拡張
汎整数拡張(はんせいすうかくちょう、英: integral promotion)[1]とは、C言語およびC++において整数の扱いをする上で、ある条件のもとにその整数の型を格上げ、あるいは格下げする変換のことをいう。JIS X 3010:2003(C99相当)では「整数拡張」(integer promotion)[2] と呼び、JIS X 3014:2003(C++03相当)では「汎整数昇格」(integral promotion)[3] と呼ぶが、意味は変わらない。
格上げ・格下げ
[編集]格上げとは、より多くの値を表現できる型へ変換することで、要はより多くのビットを持つ型への変換である。格下げとは、現在の型で表現できる最大値を表現できない型へ変換することで、要はより少ないビットを持つ型への変換である。
例として、char
型をint
型に変換するのは格上げ、int
型をchar
型に変換するのは格下げである。
格上げ
[編集]格上げをする際、変換後の値は、変換前と変換後の型および値の関係が以下のうちどれかである場合には、変換前の値が維持される。
- 「符号付き→符号付き」
- 「符号無し→符号付き」
- 「符号付き→符号無し」であり、かつ変換前の値が正の数である
- 「符号無し→符号無し」
ただし、
- 「符号付き→符号無し」であり、かつ変換前の値が負の数である
という条件の場合に限り、変換後の値は
/* 変換前の値をa、変換後の型をT型とする。T_MAXはT型の最大値。 */
(signed T)a + (1 + T_MAX)
となる。
格下げ
[編集]格下げをする際、変換前の値をa
、変換後の型をT
型とすると、変換後の値は、変換前と変換後の型および値の関係が以下のうちどちらかである場合には、変換前の値が維持される。
- 「符号付き→符号付き」であり、かつ
a
がsigned T
で表現可能である - 「符号無し→符号付き」であり、かつ
a
がsigned T
で表現可能である
変換前の値が維持されない場合を以下に列挙する。
- 「符号付き→符号付き」であり、かつ
a
がsigned T
で表現できない場合→処理系依存 - 「符号無し→符号付き」であり、かつ
a
がsigned T
で表現できない場合→処理系依存 - 「符号付き→符号無し」であり、かつ
a
が正の数である場合→
a % (1 + T_MAX)
- 「符号付き→符号無し」であり、かつ
a
が負の数である場合→
(1 + T_MAX) - (-a % (1 + T_MAX))
- 「符号無し→符号無し」である場合→
a % (1 + T_MAX)
条件と変換結果
[編集]端的に言うと、int
型あるいはunsigned int
型を使用できる式の中では、char
, short
, int
, int
ビットフィールドの符号付き・符号無しにかかわらず、それら元の型の全ての値をint
型で表現できるならばそれらの値をint
型に、それ以外はunsigned int
型に変換するということである。演算に先立って、すべてのオペランドの型が揃えられる。
例えば、unsigned char
型同士の演算では、unsigned char
型のまま演算するのではなく、unsigned char
型の値をまずint
型に格上げする変換が暗黙的に行なわれる。その後、途中の各演算はint
型で行なわれる。これにより、各演算結果がINT_MAX
を超えたりINT_MIN
未満となったりしないかぎり[注釈 1]、計算途中の値がオーバーフローして算術エラーが起こることはない。
同様に、int
型での演算結果をunsigned char
型に代入する際に格下げする変換が暗黙的に行なわれるが、最終結果がUCHAR_MAX
を超えたり0
未満となったりしないかぎり[注釈 2]、オーバーフローが起こることはない。
#include <stdio.h>
#include <limits.h>
int main(void) {
unsigned char uc1 = 100;
unsigned char uc2 = 100;
unsigned char uc3 = 0;
unsigned char uc4 = 200;
int si = uc1 * uc2; /* 10000 */
unsigned char uc5 = -(uc1 * uc2) / (uc3 - uc4); /* 50 */
printf("INT_MIN = %+d\n", INT_MIN);
printf("INT_MAX = %+d\n", INT_MAX);
printf("UCHAR_MAX = %d\n", UCHAR_MAX);
printf("si = %d\n", si);
printf("uc5 = %d\n", uc5);
return 0;
}
上記において、unsigned char
型の値はすべてint
型で表現可能であることから、先程示した汎整数拡張の規則が適用され、式uc1 * uc2
におけるuc1
とuc2
の評価結果は、いったんint
型に格上げされる。つまり、uc1 * uc2
は暗黙的に(int)uc1 * (int)uc2
とみなされる。
同様に、-(uc1 * uc2) / (uc3 - uc4)
は暗黙的に-((int)uc1 * (int)uc2) / ((int)uc3 - (int)uc4)
とみなされる。
別の例を示す。
#include <stdio.h>
#include <limits.h>
int main(void) {
unsigned char uc = UCHAR_MAX; /* UCHAR_MAX は unsigned char 型で表現できる最大値 */
int si1 = uc + 1; /* (1) */
int si2 = ++uc; /* (2) */
printf("si1 = %d\n", si1);
printf("si2 = %d\n", si2);
return 0;
}
上記(1)の代入式では、まず右辺式uc + 1
において、unsigned char
型のオブジェクトであるuc
に、int
型のリテラルである1
を加算する演算を行なう。このとき、先程と同じように汎整数拡張が適用され、uc
の評価結果は、いったんint
型に格上げされる。この処理系ではchar
型が8ビットであると仮定すれば、UCHAR_MAX
の値は255
になる。そしてこのとき、uc
の評価結果である255
はunsigned char
型ではなくint
型である。ゆえに、uc + 1
の演算結果はint
型の255 + 1
すなわち256
となり、si1
に代入される値は256
である。
一方、上記(2)の代入式では、まず右辺式++uc
において、unsigned char
型のオブジェクトであるuc
に前置インクリメント演算子が付いているから、以下のようにuc
に1
を加算した値をuc
に代入するという計算が行なわれる。
uc = uc + 1; /* ++uc の解釈 */
上記における単純代入演算子=
の右オペランドには先程と同じように汎整数拡張が適用されて、uc
の評価結果255
はint
型に変換され、そしてuc + 1
の演算結果はint
型の256
となる。その後、int
型の256
をunsigned char
型のuc
に代入する演算が行なわれる。しかしこのとき、unsigned char
型では256
を表現することはできないので、int
型の256
を、unsigned char
型に格下げする変換が行なわれることになる。
今回の変換では、前述の「格下げ」項に示した《「符号付き→符号無し」であり、かつaが正の数である場合》の規則である
a % (1 + T_MAX)
が適用される。ここで、a
は256
、T_MAX
はunsigned char
型の最大値つまりUCHAR_MAX
で255
となる。これらの値を上記の公式に代入してみると、
256 % (1 + 255)
256 % 256
となり、256を256で割った余りは0になるので、最終的にsi2
に代入される値は0
になる。つまり、情報の欠落が発生することになる。
汎整数拡張の特異性
[編集]上記「条件と変換結果」の項に示したサンプルの式(2)では、インクリメント演算子の意味と、各型で表現可能な値の範囲を知ってさえいれば、(汎整数拡張のルールについて詳しく知らずとも)結果は十分予測できる。しかし、汎整数拡張はコンパイル時に勝手に裏で行なわれる「暗黙の型変換」であるため、ルールを知らなければ意外なバグの原因となる場合がある。
#include <stdio.h>
int main(void) {
int si = -1;
unsigned int ui = 1;
printf("%d\n", si < ui);
return 0;
}
上記の例では、-1
のほうが1
より小さいことから、一見してsi < ui
の比較結果は真 (1
) となるように見えるが、実際には汎整数拡張により、si
のほうがunsigned int
に変換され、-1
がUINT_MAX
に変換されることによって、比較結果は偽 (0
) となる。とはいえ、このような例(符号付き型と符号無し型の比較)は典型的なプログラミングミスであり、通例コンパイラが警告を発する対象となる。
具体的な解決策としては、式の中で用いる変数の型を揃える、できるかぎり表現可能な値の範囲の広い型を使用する、といったことが挙げられる。
また、処理系により整数型のビット数が異なることがあるので、あるソースコードをそのまま別の処理系で動作させる際、汎整数拡張により、移植前の処理系では起こり得なかったバグが急に発生するというケースもある。この場合は、整数型のビット数に依存しない、移植性の高いソースコードを書くということが何よりの解決策となる。
他の言語
[編集]C/C++以外の言語にも、整数型および浮動小数点数型を包括した、類似の暗黙的な型昇格ルールが存在する。例えばJavaではnumeric promotionと呼ばれている。C#ではtype promotionと呼ばれている。暗黙の型昇格により、異なる型同士の演算はいったん上位の型に変換されてから実行される。ここでは簡単に述べるにとどめる。詳細はそれぞれの言語規格を参照されたい。
Javaは各整数型のビット数が規格で厳密に定められており、符号無し整数型をサポートせず、また暗黙の拡大変換 (widening conversion) はサポートするものの、暗黙の縮小変換 (narrowing conversion) はサポートしない。このため、C/C++よりもプログラミングミスが起こりにくくなっている。
byte sb1 = 100;
byte sb2 = 100;
byte sb3 = -100;
byte sb4 = 100;
int si = sb1 * sb2;
//byte sb5 = -(sb1 * sb2) / (sb3 - sb4); // Compile Error.
byte sb5 = (byte)(-(sb1 * sb2) / (sb3 - sb4)); // OK.
System.out.println("si = " + si);
System.out.println("sb5 = " + sb5);
C#は符号無し整数型をサポートするが、Java同様に暗黙の縮小変換はサポートしない。
byte ub1 = 100;
byte ub2 = 100;
byte ub3 = 0;
byte ub4 = 200;
int si = ub1 * ub2;
//byte ub5 = -(ub1 * ub2) / (ub3 - ub4); // Compile Error.
byte ub5 = (byte)(-(ub1 * ub2) / (ub3 - ub4)); // OK.
System.Console.WriteLine("si = " + si);
System.Console.WriteLine("ub5 = " + ub5);
C#では符号付き整数型と符号無し整数型の比較結果も、C/C++と違って直感的で自然なものとなる。ただし、これはより上位の符号付き整数型に型昇格されて演算されているからであり、例えば32ビット符号付き整数型int
と32ビット符号無し整数型uint
の比較は、いったん64ビット符号付き整数型long
に変換されてから実行される。64ビット符号付き整数型long
と64ビット符号無し整数型ulong
の比較はサポートされず、コンパイルエラーになる。
int si = -1;
uint ui = 1;
System.Console.WriteLine(si < ui); // True
long sl = -1L;
ulong ul = 1UL;
System.Console.WriteLine(sl < ul); // Compile Error.
一方F#など、暗黙の型昇格を許さず、異なる型同士の演算には必ず明示的な変換が事前に必要となる言語もある。
let x : int = 100
//let y : sbyte = -1 // Compile Error.
let y : sbyte = -1y // OK.
//let z : int = x + y // Compile Error.
let z : int = x + int y // OK.
//let w : int = y // Compile Error.
let w : int = int y // OK.
printfn "z = %d" z
printfn "w = %d" w