コンテンツにスキップ

Rustの整数オーバーフロー

Rustの整数型は、どれも大きさが決まっています。u8 に入るのは0から255までの数だけで、それ以外は入りません。では、255に1を足すとどうなるでしょうか? 答えが収まりきりません。これを整数オーバーフロー(integer overflow)と呼びます。

3桁しかない古い車の走行距離計を思い浮かべてください。999キロを走ると、000に戻ってしまいます。おもちゃならそれでもかまいませんが、銀行の残高だったら大変です。Rustは2つの方法で助けてくれます。開発中は、問題を大きな声で知らせてくれます。そして、代わりにどうしてほしいのかをはっきり書ける、シンプルなメソッドを用意してくれています。心配はいりません。メソッドは4種類だけで、どれも同じパターンに従っています。このページはデータ型と演算子の続きです。

整数型の大きさはそれぞれどれくらい?

Section titled “整数型の大きさはそれぞれどれくらい?”

どの型にも、いちばん小さい値(MIN)といちばん大きい値(MAX)があります。u8::MAX や i32::MIN のような定数で表示できます。

fn main() {
println!("u8: {} 〜 {}", u8::MIN, u8::MAX);
println!("i8: {} 〜 {}", i8::MIN, i8::MAX);
println!("u16: {} 〜 {}", u16::MIN, u16::MAX);
println!("i32: {} 〜 {}", i32::MIN, i32::MAX);
println!("u32: {} 〜 {}", u32::MIN, u32::MAX);
println!("i64: {} 〜 {}", i64::MIN, i64::MAX);
println!("u64: {} 〜 {}", u64::MIN, u64::MAX);
println!("u128: {} 〜 {}", u128::MIN, u128::MAX);
}

実行結果:

u8: 0 〜 255
i8: -128 〜 127
u16: 0 〜 65535
i32: -2147483648 〜 2147483647
u32: 0 〜 4294967295
i64: -9223372036854775808 〜 9223372036854775807
u64: 0 〜 18446744073709551615
u128: 0 〜 340282366920938463463374607431768211455
▶ 実際に試してみよう

整数型の一覧を表にまとめると、次のようになります。

型 ビット数 最小値 最大値
i8 / u8 8 -128 / 0 127 / 255
i16 / u16 16 -32,768 / 0 32,767 / 65,535
i32 / u32 32 約 -21億 / 0 約 21億 / 約 43億
i64 / u64 64 約 -9.2 × 10^18 / 0 約 9.2 × 10^18 / 約 1.8 × 10^19
i128 / u128 128 約 -1.7 × 10^38 / 0 約 1.7 × 10^38 / 約 3.4 × 10^38
isize / usize ほとんどのコンピュータで64 i64 と同じ / 0 i64 と同じ / u64 と同じ

符号付きの型(i)は、範囲の半分を負の数に使います。符号なしの型(u)は0から始まるので、最大値がおよそ2倍になります。この範囲から外れるものはすべてオーバーフローです。MIN を下回る場合(u8 で 0 - 1 をするなど)も含まれます。こちらはアンダーフローと呼ばれることもありますが、Rustでは同じものとして扱います。

数値がオーバーフローしたときにRustがすること

Section titled “数値がオーバーフローしたときにRustがすること”

デバッグビルドではパニックする

Section titled “デバッグビルドではパニックする”

cargo run でプログラムを実行するとき(デバッグビルド)、Rustはすべての +・-・* をチェックします。答えが収まらなければ、わかりやすいメッセージとともにプログラムが止まります。次の例では、ゲームが u8 のスコアにボーナスポイントを足しています。

fn add_points(score: u8, bonus: u8) -> u8 {
score + bonus
}
fn main() {
let total = add_points(250, 10);
println!("合計: {total}");
}

実行すると、次のように表示されます。

thread 'main' (12345) panicked at src/main.rs:2:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ 実際に試してみよう

attempt to add with overflow は「足し算でオーバーフローが起きた」という意味です。値をわざと関数の引数経由で渡しているのには理由があります。数値を + のすぐ隣に直接書くと、プログラムを実行する前にコンパイラが問題を見つけてしまうのです(このあとすぐに見ます)。プログラムが止まるのは厳しく感じるかもしれませんが、これは親切なのです。本番環境で間違ったスコアが出てしまう代わりに、自分のマシンの上で、正確な行番号つきでバグを見つけられるのですから。パニックについてはパニックのページで詳しく説明しています。

リリースビルドでは値が一周する

Section titled “リリースビルドでは値が一周する”

cargo run --release でビルドすると、コードを少しでも速くするために、このチェックがオフになります。数値は走行距離計のように一周して(wrap around)しまいます。u8 で 250 + 10 は 4 になります(260 - 256 = 4 だからです)。

Terminal window
cargo run --release
合計: 4

パニックも警告もなく、ただ黙って間違った数値になります。だからこそ、オーバーフローが「たまたま」起きることを当てにしてはいけません。一周させたいなら、wrapping_add(このあと紹介します)を使って、はっきりそう書きましょう。このサイトの「Try it」ボタンはデバッグビルドで実行するので、そこでこの動きを確かめるにも wrapping_add を使います。

fn main() {
let score: u8 = 250;
println!("{}", score.wrapping_add(10));
}

実行結果:

4
▶ 実際に試してみよう

リリースビルドでもパニックさせたい場合は、Cargo.toml でチェックをオンに戻せます。

[profile.release]
overflow-checks = true

数値を直接書いた場合は、コンパイルすらできない

Section titled “数値を直接書いた場合は、コンパイルすらできない”

計算の全体がコンパイラから見えている場合、コンパイラはビルドを拒否します。

fn main() {
let level: u8 = 255 + 1;
println!("{level}");
}
error: this arithmetic operation will overflow
--> src/main.rs:2:21
|
2 | let level: u8 = 255 + 1;
| ^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default

「この計算はオーバーフローする」「u8::MAX + 1_u8 を計算しようとしているが、オーバーフローしてしまう」というエラーです。let level: u8 = 256; のように、リテラルがそもそも大きすぎる場合も同じで、literal out of range for u8(リテラルが u8 の範囲外)というエラーになります。

オーバーフローが起きないことを祈る代わりに、起きたときにどうするかを自分で選べます。どの整数型にも、そのためのメソッドが4種類あります。どれも u8 でも i32 でも u64 でも、同じように使えます。

checked_:「収まらなかったら教えて」

Section titled “checked_:「収まらなかったら教えて」”

checked_add・checked_sub・checked_mul は Option を返します。収まれば Some(答え)、収まらなければ None です。お金のように、間違った数値が深刻な問題になる場面では、これがいちばん安全な選択です。銀行の残高は0を下回ってはいけないので、u64 と checked_sub を使えば、引き出しを自動で断ってくれます。

fn withdraw(balance: u64, amount: u64) -> Option<u64> {
balance.checked_sub(amount)
}
fn order_total(price: u32, quantity: u32, shipping: u32) -> Option<u32> {
price.checked_mul(quantity)?.checked_add(shipping)
}
fn main() {
let balance: u64 = 100;
for amount in [30, 250] {
match withdraw(balance, amount) {
Some(left) => println!("{amount}を引き出しました。残高は{left}です"),
None => println!("{amount}は引き出せません:残高が足りません"),
}
}
println!("{:?}", order_total(20, 3, 5));
println!("{:?}", order_total(2_000_000_000, 3, 5));
}

実行結果:

30を引き出しました。残高は70です
250は引き出せません:残高が足りません
Some(65)
None
▶ 実際に試してみよう

結果が Option なので、? 演算子も使えます。order_total では、掛け算がオーバーフローした時点で、関数はすぐに None を返します。

saturating_add と saturating_sub は、決して MIN や MAX を超えません。満杯になったコップのように、そこで止まるだけです。HPゲージ、音量のつまみ、在庫数など、自然な下限や上限があるものにぴったりです。

fn main() {
let health: u8 = 20;
let after_hit = health.saturating_sub(35);
println!("大ダメージを受けた後のHP: {after_hit}");
let volume: u8 = 90;
let louder = volume.saturating_add(25).min(100);
println!("音量を上げた後: {louder}");
let stock: u32 = 3;
let left = stock.saturating_sub(5);
println!("在庫の残り: {left}");
}

実行結果:

大ダメージを受けた後のHP: 0
音量を上げた後: 100
在庫の残り: 0
▶ 実際に試してみよう

音量の行には、ひと手間が必要です。saturating_add が止まるのは型そのものの限界、u8 なら255です。つまみは100で止まるので、.min(100) で自分の決めた上限に抑えています。

wrapping_:「わざと一周させる」

Section titled “wrapping_:「わざと一周させる」”

一周してくれることこそ、まさに望んでいる動き、ということもあります。254、255、0、1と進む整理券のカウンター。あるいは、数値がめちゃくちゃに混ざることが前提の、簡単なハッシュ関数。wrapping_add と wrapping_mul は、デバッグでもリリースでも、どのビルドでもこの動きをします。

fn simple_hash(text: &str) -> u32 {
let mut hash: u32 = 5381;
for byte in text.bytes() {
hash = hash.wrapping_mul(33).wrapping_add(byte as u32);
}
hash
}
fn main() {
let mut ticket: u8 = 254;
for _ in 0..4 {
println!("整理券 {ticket}");
ticket = ticket.wrapping_add(1);
}
println!("\"hello\" のハッシュ値: {}", simple_hash("hello"));
println!("\"a much longer sentence\" のハッシュ値: {}", simple_hash("a much longer sentence"));
}

実行結果:

整理券 254
整理券 255
整理券 0
整理券 1
"hello" のハッシュ値: 261238937
"a much longer sentence" のハッシュ値: 3373490575
▶ 実際に試してみよう

ハッシュ値は文字列のバイト列から計算されるので、ハッシュする文字列は英語のままにしています(日本語の文字は1文字3バイトなので、別の値になります)。長い文のほうは、ふつうの * と + で計算すると、何度も u32 をオーバーフローします。wrapping_ メソッドを使えば問題ありません。それこそがこのハッシュの狙いだからです。

overflowing_:「両方ちょうだい」

Section titled “overflowing_:「両方ちょうだい」”

overflowing_add はタプルを返します。一周した後の答えと、オーバーフローしたかどうかを表す bool です。一周した値がほしいけれど、それが起きたことも知りたい、というときに便利です。

fn main() {
let (value, overflowed) = 250u8.overflowing_add(10);
println!("{value}(オーバーフローした: {overflowed})");
let (value, overflowed) = 50u8.overflowing_add(10);
println!("{value}(オーバーフローした: {overflowed})");
}

実行結果:

4(オーバーフローした: true)
60(オーバーフローした: false)
▶ 実際に試してみよう

これはほかの3つほどは使わないでしょう。どれを選べばいいか迷ったら、checked_ を選んでおけばまず間違いありません。

as でキャストするときのオーバーフロー

Section titled “as でキャストするときのオーバーフロー”

as による型の変換は、デバッグビルドでも決してパニックしません。値が収まらなければ、黙って収まるように削られてしまいます。これに驚く人はとても多いので、気をつけて使いましょう。

fn main() {
let big: i32 = 300;
let negative: i32 = -1;
println!("300 as u8 = {}", big as u8);
println!("-1 as u32 = {}", negative as u32);
match u8::try_from(big) {
Ok(small) => println!("収まりました: {small}"),
Err(e) => println!("300 は u8 に収まりません: {e}"),
}
match u8::try_from(200) {
Ok(small) => println!("収まりました: {small}"),
Err(e) => println!("200 は u8 に収まりません: {e}"),
}
}

実行結果:

300 as u8 = 44
-1 as u32 = 4294967295
300 は u8 に収まりません: out of range integral type conversion attempted
収まりました: 200
▶ 実際に試してみよう

out of range integral type conversion attempted はRust自身のエラーメッセージで、「範囲外の整数型への変換をしようとした」という意味です。300 as u8 は下位8ビットだけを残すので、44になります。-1 as u32 は u32 の最大値になります。どちらも警告してくれません。u8::try_from は先にチェックして Result を返してくれるので、どうするかを自分で決められます。as は値が収まるとわかっているときだけに使い、プログラムの外から来た値には try_from を使う習慣をつけましょう。(浮動小数点数は動きが違い、300.7 as u8 は一周せずに255で止まります。)詳しくは型キャストと型変換のページを見てください。

つまずきやすいポイントを、まとめて紹介します。

fn main() {
println!("7 / 2 = {}", 7 / 2);
println!("-7 / 2 = {}", -7 / 2);
println!("i32::MIN.checked_abs() = {:?}", i32::MIN.checked_abs());
println!("i32::MIN.unsigned_abs() = {}", i32::MIN.unsigned_abs());
println!("10u32.checked_pow(9) = {:?}", 10u32.checked_pow(9));
println!("10u32.checked_pow(10) = {:?}", 10u32.checked_pow(10));
}

実行結果:

7 / 2 = 3
-7 / 2 = -3
i32::MIN.checked_abs() = None
i32::MIN.unsigned_abs() = 2147483648
10u32.checked_pow(9) = Some(1000000000)
10u32.checked_pow(10) = None
▶ 実際に試してみよう
  • 整数の割り算は余りを捨てる。 四捨五入はせず、ゼロの方向に切り捨てます。ですから -7 / 2 は -3 です。余りがほしいときは % を、小数がほしいときは浮動小数点数を使いましょう。
  • abs() もオーバーフローすることがある。 i32 は -2147483648 まで下がれますが、上は 2147483647 までしかありません。そのため、i32::MIN を正にした値は収まりません。i32::MIN.abs() はデバッグビルドで attempt to negate with overflow(符号反転でオーバーフローが起きた)というメッセージとともにパニックします。checked_abs() を使うか、ちゃんと収まる u32 を返してくれる unsigned_abs() を使いましょう。
  • pow はすぐに大きくなる。 10の10乗は、もう u32 には大きすぎます。checked_pow・saturating_pow・wrapping_pow も用意されています。

ある値がいつも一周する、またはいつも端で止まるべきなら、std::num::Wrapping や std::num::Saturating で包んでおけます。そうすれば、ふつうの + や - が正しく動くので、うっかり忘れることもありません。

use std::num::{Saturating, Wrapping};
fn main() {
let mut ticket = Wrapping(254u8);
ticket += 1;
ticket += 1;
ticket += 1;
println!("整理券: {ticket}");
let mut battery = Saturating(10u8);
battery -= 25;
println!("バッテリー: {battery}");
}

実行結果:

整理券: 1
バッテリー: 0
▶ 実際に試してみよう
メソッドの種類 戻り値 使う場面
+ - * 数値(デバッグではパニック、リリースでは一周) 絶対にオーバーフローしないと確信できるとき
checked_add など Option<T> 間違った数値がバグになるとき:お金、サイズ、ID
saturating_add など 数値(MIN か MAX で止まる) 自然な下限・上限があるとき:HP、音量、在庫
wrapping_add など 数値(一周する) 一周することが目的のとき:ハッシュ、循環するカウンター
overflowing_add など (T, bool) 一周した値と、一周したかどうかの両方がほしいとき
T::try_from(x) Result<T, _> 小さい型に収まらないかもしれない値を変換するとき

len() は usize を返しますが、usize は負の数になれません。空のリストでは len() - 1 が -1 になるので、オーバーフローします。

fn last_index(tasks: &[String]) -> usize {
tasks.len() - 1
}
fn main() {
let tasks: Vec<String> = Vec::new();
println!("最後のタスクのインデックスは{}です", last_index(&tasks));
}

実行すると、次のように表示されます。

thread 'main' (12345) panicked at src/main.rs:2:5:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ 実際に試してみよう

attempt to subtract with overflow は「引き算でオーバーフローが起きた」という意味です。これは、ほとんどの人が一度は引っかかる間違いです。直すには、リストが空かもしれないことを型で表しましょう。tasks.len().checked_sub(1) を返せば、Option<usize> になります。最後の要素がほしいだけなら、tasks.last() を使うほうがもっと簡単です。

小さい型で小さい数を足し合わせる

Section titled “小さい型で小さい数を足し合わせる”

90、85、100という3つのテストの点数は、どれも u8 に収まります。でも、合計の275は収まりません。そのため、let total: u8 = scores.iter().sum(); は attempt to add with overflow でパニックします。パニックの場所は標準ライブラリの中を指すので混乱するかもしれませんが、原因はあなたの u8 です。足す前に、それぞれの値を大きい型に変換しましょう。

fn main() {
let scores: Vec<u8> = vec![90, 85, 100];
let total: u32 = scores.iter().map(|&s| s as u32).sum();
println!("合計: {total}、平均: {}", total / 3);
}

実行結果:

合計: 275、平均: 91
▶ 実際に試してみよう

u8 から u32 への as による変換は、いつでも安全です。どんな u8 の値も u32 に収まるからです。

1. スマートフォンのバッテリー

Section titled “1. スマートフォンのバッテリー”

スマートフォンのバッテリー残量は、0から100までの u8 です。100を超えず、0を下回らない charge(level: u8, amount: u8) -> u8 と drain(level: u8, amount: u8) -> u8 を書いてください。80から始めて、30充電し、次に130消費して、それぞれの段階で残量を表示しましょう。ヒント: 充電には saturating_add と .min(100)、消費には saturating_sub を使います。

解答を見る
fn charge(level: u8, amount: u8) -> u8 {
level.saturating_add(amount).min(100)
}
fn drain(level: u8, amount: u8) -> u8 {
level.saturating_sub(amount)
}
fn main() {
let mut level: u8 = 80;
println!("最初: {level}%");
level = charge(level, 30);
println!("充電した後: {level}%");
level = drain(level, 130);
println!("長い一日の後: {level}%");
}

実行結果:

最初: 80%
充電した後: 100%
長い一日の後: 0%
▶ 実際に試してみよう

saturating_sub は、それだけで0で止まります。充電のほうは型の限界が255なので、.min(100) で自分の決めた上限を上乗せしています。

2. 自分の限界を知っている階乗

Section titled “2. 自分の限界を知っている階乗”

1 × 2 × … × n を計算し、答えが u64 に収まらなくなったら None を返す fn factorial(n: u64) -> Option<u64> を書いてください。5、20、21について結果を表示しましょう。ヒント: let mut result: u64 = 1; から始めて、for ループの中で result.checked_mul(i)? を使います。

解答を見る
fn factorial(n: u64) -> Option<u64> {
let mut result: u64 = 1;
for i in 1..=n {
result = result.checked_mul(i)?;
}
Some(result)
}
fn main() {
for n in [5, 20, 21] {
match factorial(n) {
Some(value) => println!("{n}! = {value}"),
None => println!("{n}! は u64 には大きすぎます"),
}
}
}

実行結果:

5! = 120
20! = 2432902008176640000
21! は u64 には大きすぎます
▶ 実際に試してみよう

掛け算が1回でも収まらなくなった瞬間、? が関数全体から None を返します。u64 に入る最大の階乗は20!です。

  • どの整数型にも、T::MIN から T::MAX までの決まった範囲があります。その外に出ることがオーバーフローです。
  • デバッグビルドでは attempt to add with overflow でパニックします。リリースビルドでは黙って一周します。コンパイラから見えるオーバーフローはコンパイルエラーになります。
  • 動作は自分で選べます。checked_ は Option を返し、saturating_ は端で止まり、wrapping_ は一周し、overflowing_ は値と bool を返します。
  • as は何もチェックせず、黙って数値を変えてしまうことがあります。値が収まらないかもしれないときは T::try_from を使いましょう。
  • 空のリストでの len() - 1、小さい型での合計、MIN の abs()、そして pow に気をつけましょう。

理解度チェック

1. 値が変数から来る場合、デバッグビルドで 250u8 + 10 はどうなりますか?

2. 100u64.checked_sub(250) は何を返しますか?

3. HPゲージは決して0を下回ってはいけません。いちばん合っているメソッドはどれですか?

4. 300_i32 as u8 の値は何ですか?

スコア: 0 / 4