汎整数拡張

汎整数拡張（はんせいすうかくちょう、）とは、C言語およびC++において整数の扱いをする上で、ある条件のもとにその整数の型を格上げ、あるいは格下げする変換のことをいう。JIS X 3010:2003（C99相当）では「整数拡張」(integer promotion) と呼び、JIS X 3014:2003（C++03相当）では「汎整数昇格」(integral promotion) と呼ぶが、意味は変わらない。

格上げ・格下げ

格上げとは、より多くの値を表現できる型へ変換することで、要はより多くのビットを持つ型への変換である。格下げとは、現在の型で表現できる最大値を表現できない型へ変換することで、要はより少ないビットを持つ型への変換である。

例として、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未満となったりしないかぎり、計算途中の値がオーバーフローして算術エラーが起こることはない。

同様に、int型での演算結果をunsigned char型に代入する際に格下げする変換が暗黙的に行なわれるが、最終結果がUCHAR_MAXを超えたり0未満となったりしないかぎり、オーバーフローが起こることはない。

#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の評価結果は、いったんint型に格上げされる。つまり、は暗黙的にとみなされる。

同様に、は暗黙的にとみなされる。

別の例を示す。
#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)の代入式では、まず右辺式において、unsigned char型のオブジェクトであるucに、int型のリテラルである1を加算する演算を行なう。このとき、先程と同じように汎整数拡張が適用され、ucの評価結果は、いったんint型に格上げされる。この処理系ではchar型が8ビットであると仮定すれば、UCHAR_MAXの値は255になる。そしてこのとき、ucの評価結果である255はunsigned char型ではなくint型である。ゆえに、の演算結果はint型の255 + 1すなわち256となり、si1に代入される値は256である。

一方、上記(2)の代入式では、まず右辺式において、unsigned char型のオブジェクトであるucに前置インクリメント演算子が付いているから、以下のようにucに1を加算した値をucに代入するという計算が行なわれる。

uc = uc + 1; /* ++uc の解釈 */

上記における単純代入演算子=の右オペランドには先程と同じように汎整数拡張が適用されて、ucの評価結果255はint型に変換され、そしての演算結果は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より小さいことから、一見しての比較結果は真 (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

脚注

注釈

出典

関連項目
 C言語
 C++
 整数型
 型変換

外部リンク 
 INT02-C. 整数変換のルールを理解する

C言語
C++