Integers, floats, overflow and casts
Rust has fixed-width integers, and the width is part of the type. That makes the range of every value a compile-time fact: i8 holds minus one hundred and twenty-eight to one hundred and twenty-seven, and a literal outside the range is a build error. The previous lessons used i32 and u32 without saying much; this one says what the widths mean.
Floats are f32 and f64, and they follow the same bit layout as the C types of the same width. Their arithmetic is not exact, which the second section shows with the smallest example that surprises people. The lesson ends with the explicit ways to handle an overflow and with the error a literal of the wrong size produces.
The ranges of the integer types
The associated constants MIN and MAX are on every integer type, so the range is one print away. A signed byte and an unsigned byte have the same width and different ranges, and i32 is the type an unannotated integer literal gets.
The program prints three pairs. The values are the ones in the type names: eight bits for the byte, thirty-two for the i32, with the signed types splitting their range around zero.
fn main() {
println!("{} {}", i8::MIN, i8::MAX);
println!("{} {}", u8::MIN, u8::MAX);
println!("{} {}", i32::MIN, i32::MAX);
}
rustc --edition 2024 ranges.rs
./ranges
-128 127
0 255
-2147483648 2147483647
usize and isize are the pointer width, so their ranges differ between a 32-bit and a 64-bit target. They are the types of lengths and indices, which is why len returns a usize.
Floats and their precision
A float literal is f64 unless the context says otherwise. The sum of 0.1 and 0.2 is not exactly 0.3, and the printed form shows it for the wider type: the f32 rounds to a shorter decimal, the f64 keeps enough digits to expose the difference.
The program prints both sums and then the f64 one rounded to two decimals by the format specification. The last line is what a report should print, and the second line is why the value is not compared with == in serious code.
fn main() {
let a: f32 = 0.1 + 0.2;
let b: f64 = 0.1 + 0.2;
println!("{a} {b}");
println!("{:.2}", b);
}
rustc --edition 2024 floats.rs
./floats
0.3 0.30000000000000004
0.30
An exact decimal type is not in the standard library, which is a decision worth knowing before a program handles money. The lesson on formatting shows the rest of the float specification.
Checked, wrapping and saturating
The arithmetic operators panic on overflow in a debug build and wrap in a release build, which the lesson on variables shows. When the behaviour must not depend on the profile, the integer types have methods that say what to do.
checked_add returns an Option, wrapping_add returns the wrapped value, saturating_add stops at the maximum, and overflowing_add returns the value and a flag. The program calls all four on the maximum byte.
fn main() {
let max = u8::MAX;
println!("{:?}", max.checked_add(1));
println!("{:?}", max.wrapping_add(1));
println!("{:?}", max.saturating_add(1));
println!("{:?}", max.overflowing_add(1));
}
rustc --edition 2024 checked.rs
./checked
None
0
255
(0, true)
The debug format prints the Option as None and the tuple with its flag, which is enough to see the four different answers. The lesson on Option explains how to handle the None case properly.
Casts with as
as converts between numeric types and truncates when the target is narrower. Three hundred becomes forty-four as a u8, minus one becomes the largest u32, and a float loses its fractional part. The conversions never fail and never panic.
The program prints all three. The second line is the one to remember: a negative signed value cast to an unsigned type keeps its bit pattern, which is a definition rather than undefined behaviour.
fn main() {
let big: i32 = 300;
println!("{}", big as u8);
let negative: i32 = -1;
println!("{}", negative as u32);
let ratio = 3.99_f64;
println!("{}", ratio as i32);
}
rustc --edition 2024 casts.rs
./casts
44
4294967295
3
A conversion that must report a value out of range uses try_into instead, which returns a Result; the lesson on Result covers the handling side.
A literal that does not fit
A literal is checked against its type at compile time. Three hundred in a u8 is an error from the overflowing_literals lint, which is denied by default, and the note gives the range. The value is not truncated silently.
The program below asks for a byte and writes three hundred. The message names the literal, the type and the range, which is all a reader needs to fix it.
fn main() {
let value: u8 = 300;
println!("{value}");
}
rustc --edition 2024 out_of_range_err.rs
error: literal out of range for `u8`
--> out_of_range_err.rs:2:21
|
2 | let value: u8 = 300;
| ^^^
|
= note: the literal `300` does not fit into the type `u8` whose range is `0..=255`
= note: `#[deny(overflowing_literals)]` on by default
error: aborting due to 1 previous error
A value computed at run time does not get this check; it panics in a debug build instead, which is why the checked methods above exist for input that comes from outside the program.
Exercises
- Print the minimum and maximum of i64 and compare them with the values of i32. How many times larger is the range?
- Add 0.1 to 0.2 in a loop ten times and print the result. Is it exactly one?
- Use checked_mul on a large number and explain what the program does when the multiplication overflows.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the range of a u8?
Answer
0 to 255
An unsigned byte has eight bits and no sign, so its values run from zero to 255.
-
Why is
0.1 + 0.2not exactly 0.3 in a float?Answer
Because the sum is rounded to a binary fraction
A binary float cannot represent one tenth exactly, so the stored sum is the nearest value.
-
What does checked_add return?
Answer
The value, or None when it would overflow
The checked methods report the overflow in the return type instead of panicking or wrapping.
-
What does
300 as u8produce?Answer
44
The cast keeps the low eight bits of 300, which is 44.
-
What does this program print?
fn main() { println!("{} {}", i8::MIN, i8::MAX); println!("{} {}", u8::MIN, u8::MAX); }Answer
-128 127 / 0 255
A signed byte splits its 256 values around zero, and an unsigned one starts at zero.
-
What does this program print?
fn main() { let a: f32 = 0.1 + 0.2; let b: f64 = 0.1 + 0.2; println!("{a} {b}"); }Answer
0.3 0.30000000000000004
The f32 rounds to fewer digits and prints 0.3, while the f64 shows the stored value.
-
What does this program print?
fn main() { let max = u8::MAX; println!("{:?}", max.checked_add(1)); println!("{:?}", max.wrapping_add(1)); println!("{:?}", max.saturating_add(1)); }Answer
None / 0 / 255
Checked reports None, wrapping goes to zero, and saturating stays at the maximum.
-
What does this program print?
fn main() { let big: i32 = 300; println!("{}", big as u8); let negative: i32 = -1; println!("{}", negative as u32); let ratio = 3.99_f64; println!("{}", ratio as i32); }Answer
44 / 4294967295 / 3
The byte keeps the low bits, the negative number becomes all ones, and the float truncates.
-
What is the first line of the compiler output for this program?
fn main() { let value: u8 = 300; println!("{value}"); }Answer
error: literal out of range for
u8The literal is checked against the type, and the note gives the range of a u8.
-
What does this program print?
fn main() { let a = 7; let b = 2; println!("{}", a / b); println!("{}", 7.0 / 2.0); }Answer
3 / 3.5
Integer division truncates to three, and the float division keeps the half.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev