2147483647 に 1 を足すとどうなるか:言語ごとに違う整数オーバーフロー
Java の int、Go の int32、Rust の i32 では 2147483647 + 1 が範囲を超えます。Java と Go は負の数、Rust は既定の Cargo 設定なら dev でパニック、release で負の数。C の INT_MAX + 1 は未定義動作、Python は 2147483648 です。
32 ビット符号付き整数に入る最大値は 2,147,483,647 です。Java の int、Go の int32、Rust の i32 がこの範囲を使います。これに 1 を足したとき、プログラムが当てにしてよい結果を決めるのは言語と型の規則です。言語が型の幅を決めていない場合は、実装や対象プラットフォームも結果に影響します。同じ足し算でも、Java では負の数になり、Rust ではビルド設定によってプログラムが止まり、C では結果が定められていません。
| 言語と型 | 符号付き整数が範囲を超えたとき |
|---|---|
Java int(常に 32 ビット) |
下位ビットだけが残る(-2,147,483,648)。例外なし |
Go int32 |
下位ビットだけが残る(-2,147,483,648)。パニックなし |
Rust i32 |
オーバーフロー検査が有効ならパニック、無効なら 2 の補数で折り返した値(-2,147,483,648) |
C int(INT_MAX + 1) |
未定義動作。int の幅は実装によって異なりうる |
Python int |
任意精度。通常の整数演算に固定幅のオーバーフローはない |
Java と Go:黙って一周する
Java の int は常に 32 ビットです(JLS 4.2.1)。Java 言語仕様(JLS 15.18.2)によると、整数の加算がオーバーフローしたときの結果は、数学的な和を十分に大きな 2 の補数形式で表したときの下位ビットです。そのため、結果の符号が本来の和の符号と異なることがあります。JLS 4.2.2 には、整数演算子はオーバーフローをいかなる形でも通知しないと書かれています。
そのため Integer.MAX_VALUE + 1 は、例外を出さずに Integer.MIN_VALUE、つまり -2,147,483,648 になります。
オーバーフローを検出したいときは Math.addExact を使います。結果が int または long の範囲を超えると ArithmeticException をスローします。
int a = Integer.MAX_VALUE;
int wrapped = a + 1; // -2147483648
int checked = Math.addExact(a, 1); // ArithmeticException
Go 仕様の「Integer overflow」の項も似ています。符号付き整数の +、-、*、/、<< はオーバーフローしてもかまいません。結果は整数の表現方法、演算、オペランドで決まり、実行時パニックは起きません。Go の n ビット整数は 2 の補数で表されるので、int32 の最大値に 1 を足すと -2,147,483,648 になります。
仕様は、コンパイラがオーバーフローは起きないと仮定して最適化してはならないとも定めています。たとえば x < x + 1 が常に真だと仮定することはできません。
var a int32 = math.MaxInt32
b := a + 1 // -2147483648
型は必ず確認してください。Go の int は実装によって 32 ビットか 64 ビットです。int が 64 ビットなら、2147483647 + 1 はオーバーフローせず 2,147,483,648 になります。
これは実行時に計算される値の話です。Go の定数式は正確に計算され、型付き定数はその型で正確に表現できなければなりません。範囲外の型付き定数はコンパイルエラーになります。
Rust:ビルド設定で変わる
Rust リファレンスは算術オーバーフローをプログラマの誤りとして扱います。debug_assert! が有効なビルドでは、実装はオーバーフローを検査してパニックさせなければなりません。それ以外のビルドでは、パニックにするか黙って折り返すかを実装が選べます。折り返す場合は 2 の補数の規則に従う必要があります。
言語の規則とは別に、Cargo のプロファイルが一般的な既定値を決めます。Cargo の既定の dev プロファイルは overflow-checks = true、既定の release プロファイルは overflow-checks = false です。どちらもプロジェクト側で変更できます。既定のプロファイルのままなら、i32::MAX + 1 は開発ビルドでパニックし、release ビルドでは -2,147,483,648 を返します。
どの動作を望むかは、メソッド名でコードに残せます。
let a = i32::MAX;
a.checked_add(1); // None
a.wrapping_add(1); // i32::MIN
a.saturating_add(1); // i32::MAX
C:符号付き整数には約束がない
C 規格は int が 32 ビットだとは定めていません。そのため、C では INT_MAX + 1 という式で考えるほうが正確です。2,147,483,647 が INT_MAX になるのは、int の最大値がその値である実装だけです。
C23 の作業草案 N3220 の 6.5.1 によると、式の評価結果がその型で表現できる範囲を外れた場合、動作は未定義です。未定義動作とは、規格が何の要求も課さない動作です。INT_MAX + 1 が INT_MIN になる保証も、プログラムが止まる保証もありません。
符号なし整数は事情が違います。6.2.5 によると、符号なしの演算は 2^N を法として行われるためオーバーフローせず、UINT_MAX + 1u は 0 です。
C でオーバーフローを検出するには、足す前に範囲を比べます。b > 0 のとき a > INT_MAX - b なら a + b はオーバーフローします。GCC の __builtin_add_overflow を使う方法もあります。
int r;
if (__builtin_add_overflow(INT_MAX, 1, &r)) {
/* オーバーフロー時の処理 */
}
GCC のマニュアルによると、この関数は 2 つのオペランドを無限精度で足し、第 3 引数が指す変数に変換後の結果を格納します。格納された値が無限精度の結果と異なれば true を返します。
Python:任意精度の整数
Python のドキュメントによると、int の精度に制限はありません。通常の整数の足し算は固定幅の境界でオーバーフローせず、2147483647 + 1 は 2147483648 になります。
ただし、固定幅の整数に変換するときや、固定幅の値を求めるインターフェースに渡すときは範囲の確認が必要です。
32 ビットの境界が見える場所
2,147,483,647 は 16 進数で 7fffffff、2,147,483,648 は 80000000 です。7fffffff に 1 を足して 80000000 になると、最上位ビットが 1 になり、下位 31 ビットはすべて 0 になります。32 ビットの 2 の補数では、このビットパターンが最小値 -2,147,483,648 を表します。進数変換ツールで 2 つの値を確認できます。
符号付き 32 ビットの UNIX 時間に関する話は、UNIX時間と2038年問題に続きます。割り算で言語ごとに結果が分かれる例は、負の数の剰余で扱っています。
ほかの言語から合計やカウンタの計算を移植するときは、まず値の型と幅を確認してください。次に、オーバーフローをエラーとみなすのか、折り返した値でよいのかを決めます。そのうえで Math.addExact、checked_add、wrapping_add のように、その選択が名前に表れる関数を使います。
参照した仕様とドキュメント
- JLS SE 21 §4.2
- JLS SE 21 §15.18.2
- Java
Math.addExact - Go specification: Integer overflow
- Go specification: Numeric types
- Go specification: Constant expressions
- Rust Reference: Overflow
- Rust Reference: Behavior not considered
unsafe - Cargo Book: Profiles
- Rust
i32 - C23 作業草案 N3220
- GCC: Integer Overflow Builtins
- Python 標準ライブラリ:Numeric Types