- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
同じ書き方でも、何が起きるかはその言語が約束した分しか決まっていません。今回は整数の足し算がその例です。
まず結論
固定幅の整数には表せる範囲の端があり、端を越えた後に何が起きるかは言語ごとに約束が違います。折り返す(Go、C の符号なし、Rust の release は rustc の選択)、止まる(Rust の debug)、そもそもあふれない(Python の int)、そして C の符号付きは「未定義」です。
未定義は「おかしな値になる」という意味ではありません。gcc 13.5.0 は、あふれを調べるつもりの `x + 1 < x` を -O0 でも「常に0を返す」コードに置き換えました。検査そのものが消えたのです。
C の関数 `int will_overflow(int x) { return x + 1 < x; }` に `INT_MAX` を渡します。gcc 13.5.0 の -O0(最適化なし)でコンパイルして実行すると、戻り値は何でしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
端を越えた後の4通り
第2週では、ビット数が決まると表せる範囲が決まり、越えると上位の桁が捨てられて静かに間違った値になる例を見ました。今回はその先です。上位の桁が捨てられることさえ、言語によっては約束されていません。
先に表し方を1段落だけ。固定幅の符号付き整数は、ほぼすべてのコンピュータで2の補数で表します。ビットを反転して1を足した値を負数として扱う方式で、8ビットなら 11111111 が -1 です。C は C23 から規格上もこの表現に限りました(NOTE 2 The sign representation defined in this document is called two’s complement.、それ以前の版は他の表現も許していた)。詳しくは CS クイズ「データ表現と数値」の問4・問5 で確かめられます。
32ビットの符号付き整数の上限は 2147483647 です。ここに1を足したときに起きることは、言語(と実装)によって4通りに分かれます。
- ・「未定義」は4つ目の値ではない。規格が何も約束しないので、処理系は検査の式ごと書き換えてよい
- ・C の符号なしの例は 0 − 1(4294967295 になる)。上限 + 1 ではない
- ・JS の Number は倍精度なので、この大きさの足し算では折り返さない。32ビットになるのはビット演算のとき
それぞれ一次ソースに書かれています。Go の仕様は符号付きについて may legally overflow and the resulting value exists and is deterministically defined と書き、Overflow does not cause a run-time panic. と続けます。Rust の Reference は Integer operators will panic when they overflow when compiled in debug mode.。ただし debug 以外については Other kinds of builds may result in panics or silently wrapped values on overflow, at the implementation’s discretion. と実装に任せ、あふれ自体を the programmer has made an error と位置づけます。約束されているのは、折り返すなら2の補数で結果が定まる(well-defined)ところまでです。Python は Integers have unlimited precision.。
同じ足し算を5言語で動かす
Python 3.14.7 / Node v24.21.0 / Go 1.27.1 / rustc 1.98.1 / gcc 13.5.0、いずれも Docker 上の aarch64 です。
# a.pyprint(2**31 - 1 + 1, 2**64, (2**64).bit_length())// a.jsconsole.log((2**31) | 0, 2**31 - 1 + 1, 2n**64n);// main.govar x int32 = math.MaxInt32; x++var u uint8 = 0; u--// main.rs(x は実行時に i32::MAX になる)println!("checked: {:?}", x.checked_add(1));println!("wrapping: {}", x.wrapping_add(1));println!("plain: {}", x + 1); // 5行目// o.cint will_overflow(int x) { return x + 1 < x; }unsigned u = 0; u = u - 1;
--- $ python /w/a.py2147483648 18446744073709551616 65sys.int_info(bits_per_digit=30, sizeof_digit=4, default_max_str_digits=4300, str_digits_check_threshold=640)[exit 0]--- $ node /w/a.js-2147483648 2147483648 18446744073709551616n[exit 0]--- $ go run /w/main.goint32 max+1: -2147483648uint8 0-1: 255[exit 0]--- $ rustc /w/main.rs -o /tmp/m && /tmp/m (debug default)thread 'main' (25) panicked at /w/main.rs:5:27:attempt to add with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtracechecked: Nonewrapping: -2147483648[exit 101]--- $ gcc -O0 /w/o.c -o /tmp/x && /tmp/xINT_MAX+1 check: 0unsigned 0-1: 4294967295[exit 0]
Go は折り返し、Rust の既定は5行目の x + 1 で止まり(exit 101、plain: の行は出ない)、Python は 65 ビットの値をそのまま持ちます。Rust の checked_add は None を返し、wrapping_add は折り返しを明示的に選ぶ書き方です。
同じ Rust のソースを rustc -O でビルドすると plain: -2147483648 で exit 0、cargo run --release も同じでした。止まるかどうかはビルドの設定しだいで、release で折り返すのは言語の約束ではなく rustc の選択です。
そして C の検査は、INT_MAX を渡したのに 0 でした。
同じ Rust のソース(x は実行時に i32::MAX)の `x + 1` が、rustc の既定ではパニックし、`rustc -O` では -2147483648 を出しました。この差は何によるものですか。
未定義は「何でもよい」であって「折り返す」ではない
C の符号付きの規則は N3220 §6.5.1p5 です。
If an exceptional condition occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not in the range of representable values for its type), the behavior is undefined.
結果が型の範囲に収まらなければ、ふるまいは未定義です。規格は「折り返す」とも「止まる」とも言っていません。あふれが起きない前提で式を書き換えても、規格には反しません。 その前提に立つと、x + 1 < x は常に偽です。
gcc がどの段階でそう判断したのかは、ここでは確かめていません。確かめたのは出力です。
対照的に、Go の仕様はこの書き換えを名指しで禁じています。
A compiler may not optimize code under the assumption that overflow does not occur. For instance, it may not assume that
x < x + 1is always true.
同じ C でも符号なしは別の約束です(§6.2.5p11、arithmetic for the unsigned type is performed modulo 2N)。型を変えるだけで、同じ - 1 が定義された折り返しになります。
消えた比較と、残った比較
gcc 13.5.0(aarch64)が出した will_overflow のアセンブリです。
$ gcc -O0 -S -o - /w/o.csub sp, sp, #16str w0, [sp, 12]mov w0, 0add sp, sp, 16ret$ gcc -O0 -fwrapv -S -o - /w/o.csub sp, sp, #16str w0, [sp, 12]ldr w1, [sp, 12]mov w0, 2147483647cmp w1, w0cset w0, eqand w0, w0, 255add sp, sp, 16ret
-O0 では引数を保存した直後に mov w0, 0 で 0 を返します。-fwrapv(assume that signed arithmetic overflow ... wraps around using twos-complement representation)を付けると比較が戻り、「x が 2147483647 と等しいか」に変わりました。x86_64 でも -O0 は movl $0, %eax、-fwrapv では cmpl $2147483647, -4(%rbp) で、ISA による差はありませんでした。
他の指定も両 ISA で同じです。
指定 結果-O0 / -O2 check: 0-O0 -fwrapv / -O2 -fwrapv check: 1-O0 -ftrapv Aborted、exit 134-O0 -fsanitize=undefined runtime error を出して続行、check: 1、exit 0-O0 -Wall -Wextra 警告なしmain 内に if (m+1<m) no overflow(-fwrapv で overflow detected)値を argc から作る -O0 でも 0
値が実行時に決まる形にしても、検査を main に直接書いても、-O0 で結果は変わりませんでした。
gcc 13.5.0 で `x + 1 < x` の検査が消えた実演について、正しい説明はどれですか。
C の int(gcc 13.5.0)
規格は結果を未定義とし、何も約束しません。gcc は x + 1 < x を常に0を返すコードにし、-O0 でも比較命令は残りませんでした。-fwrapv を付けたときだけ比較が残りました。
gcc -O0 INT_MAX+1 check: 0gcc -O0 -fwrapv INT_MAX+1 check: 1
Go の int32(Go 1.27.1)
仕様が折り返しを定め、x < x + 1 を常に真と仮定する最適化を禁じています。実演では上限に1を足すと -2147483648 になりました。止まりはしませんが、値は約束どおりです。
int32 max+1: -2147483648uint8 0-1: 255
どちらも静かに負の値や別の値になりうる言語です。それでも、あふれを調べる検査を書いたときの安全さが違うのはなぜでしょうか。
「あふれた後の値」ではなく、「あふれた後について仕様が何を書いているか」を比べてください。
この章のまとめ
- 端を越えた後は4通り。折り返す(Go、C の符号なし、Rust release は rustc の選択)、止まる(Rust debug)、あふれない(Python の int)、未定義(C の符号付き)。
- 未定義は「折り返す」ではない。gcc 13.5.0 は
x + 1 < xを -O0 でも 常に0にし、aarch64 と x86_64 の両方で同じだった。 - Go の仕様はその書き換えを名指しで禁じる。同じ検査の安全さは、値ではなく約束で決まる。
- Rust の止まる・止まらないはビルド設定(overflow-checks)で変わる。
checked_addは約束をコードに書く方法。 - UBSan は報告して続ける。止めるのは -ftrapv。
- JS は足し算では折り返さず、ビット演算のときだけ32ビットになる。2^53 の話は第7期で扱う。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
C で `unsigned u = 0; u = u - 1;` が 4294967295 になるのは、規格の上でどう扱われているからですか。
用語集(3語)
- 2の補数
- 負の整数を、ビットを反転して1を足した値で表す方式。引き算を同じ加算回路で扱え、0 の表現が1通りに定まる。C は C23(N3220)から規格上もこの表現に限定した。
- 未定義動作
- 規格が結果を何も定めないふるまい。C の符号付き整数のあふれはこれにあたる(N3220 §6.5.1p5)。「おかしな値になる」とも決まっていない。
- 折り返し(wrap around)
- 上限を越えた値が、あふれた上位ビットを捨てた値になること。Go の仕様と C の符号なしが約束している。Rust の release(rustc)も折り返すが、Reference は panic か折り返しかを実装の裁量とし、あふれ自体を誤りと位置づける。
出典(一次ソース)
- ISO/IEC 9899:2024 working draft N3220 — §6.5.1 General p5、§6.2.5 Types p11、§6.2.6.2 Integer types p6 ↗
- GCC 13.5.0 Manual — Options for Code Generation Conventions(-fwrapv / -ftrapv) ↗
- GCC 13.5.0 Manual — Program Instrumentation Options(-fsanitize=undefined) ↗
- The Rust Reference — Operator expressions, Overflow ↗
- The Rust Reference — Behavior not considered unsafe, Integer overflow ↗
- The Rust Programming Language — 3.2 Data Types ↗
- The Go Programming Language Specification(Language version go1.27 (May 26, 2026))— Integer overflow ↗
- Python 3.14 documentation — Built-in Types, Numeric Types ↗
- ECMA-262 Draft(September 20, 2026)— §7.1.8 ToInt32 ↗