Floating-Point Types
Rust provides two primitive floating-point types for representing real numbers with fractional parts: f32 (32-bit single-precision) and f64 (64-bit double-precision). Both types conform to the IEEE 754-2008 standard for binary floating-point arithmetic (binary32 and binary64).
f32 and f64 Overview
When a floating-point literal such as 3.14 is written without a type suffix or external type constraint, Rust defaults to f64 because on modern 64-bit CPUs, f64 executes at virtually the same speed as f32 while offering more than double the decimal precision.
| Property | f32 (Single Precision) |
f64 (Double Precision) |
|---|---|---|
| Total size | 32 bits (4 bytes) | 64 bits (8 bytes) |
| Sign / Exponent / Fraction bits | 1 / 8 / 23 bits | 1 / 11 / 52 bits |
Decimal digits of precision (DIGITS) |
~6 to 9 decimal digits (f32::DIGITS == 6) |
~15 to 17 decimal digits (f64::DIGITS == 15) |
Largest finite value (MAX) |
≈ 3.4028235 × 10³⁸ |
≈ 1.7976931348623157 × 10³⁰⁸ |
Smallest positive normal (MIN_POSITIVE) |
≈ 1.1754944 × 10⁻³⁸ |
≈ 2.2250738585072014 × 10⁻³⁰⁸ |
Machine epsilon (EPSILON) |
≈ 1.1920929 × 10⁻⁷ ($2^{-23}$) |
≈ 2.220446049250313 × 10⁻¹⁶ ($2^{-52}$) |
fn main() {
let default_float = 2.5; // Inferred as f64
let single_prec: f32 = 2.5; // Annotated as f32
let suffixed = 2.5_f32; // Suffixed literal
println!("f64 pi: {:.15}", std::f64::consts::PI);
println!("f32 pi: {:.15}", std::f32::consts::PI);
println!("Values: {default_float}, {single_prec}, {suffixed}");
}
f64 pi: 3.141592653589793
f32 pi: 3.141592741012573
Values: 2.5, 2.5, 2.5
Notice that f32 loses accuracy after 7 decimal digits (3.1415927...), whereas f64 remains accurate across 15–17 decimal digits.
IEEE 754 Binary Layout
Every floating-point value is stored in scientific notation in base 2:
$$\text{value} = (-1)^{\text{sign}} \times (1.\text{fraction})_2 \times 2^{\text{exponent} - \text{bias}}$$
f32 (32 bits total):
+------+-------------------+-----------------------------------+
| Sign | Exponent | Fraction (Mantissa) |
| 1 b | 8 bits (bias 127) | 23 bits (+1 implicit) |
+------+-------------------+-----------------------------------+
[31] [30..23] [22..0]
f64 (64 bits total):
+------+--------------------+----------------------------------+
| Sign | Exponent | Fraction (Mantissa) |
| 1 b | 11 bits (bias 1023)| 52 bits (+1 implicit) |
+------+--------------------+----------------------------------+
[63] [62..52] [51..0]
Floating-Point Literals
A floating-point literal can be written with a decimal point, scientific exponent notation (e or E), visual underscores (_), or a type suffix (f32 or f64):
fn main() {
let standard = 3.14159;
let trailing_dot = 5.; // Valid f64 literal (5.0), though `5.0` is preferred
let scientific = 6.022e23_f64; // 6.022 × 10²³
let micro = 2.5e-6; // 0.0000025
let integer_with_suffix = 42f32; // Integer digits with `f32` suffix -> 42.0_f32
let readable = 1_000_000.000_1;
println!("{standard}, {trailing_dot}, {scientific:e}, {micro}, {integer_with_suffix}, {readable}");
}
3.14159, 5, 6.022e23, 0.0000025, 42, 1000000.0001
Note: While
5.is a valid float literal (5.0), calling a method directly on5.(such as5..sqrt()) is parsed as the range operator..(5 .. sqrt()). Always write5.0.sqrt()or5_f64.sqrt().
Binary Representation Rounding and Equality
Many everyday base-10 fractions—such as 0.1, 0.2, and 0.3—are repeating fractions in base 2 ($\frac{1}{10} = 0.0001100110011\ldots_2$). Because the mantissa has a finite number of bits, these numbers are rounded to the nearest representable binary fraction.
As a result, 0.1 + 0.2 does not equal 0.3 bit-for-bit:
fn main() {
let sum: f64 = 0.1 + 0.2;
let target: f64 = 0.3;
println!("0.1 + 0.2 = {sum:.17}");
println!("0.3 = {target:.17}");
println!("Direct == : {}", sum == target);
// Compare using an absolute tolerance instead of `==`
let close_enough = (sum - target).abs() < 1e-12;
println!("Within 1e-12: {close_enough}");
}
0.1 + 0.2 = 0.30000000000000004
0.3 = 0.29999999999999999
Direct == : false
Within 1e-12: true
For financial calculations where exact base-10 arithmetic is required, represent amounts as integer smallest-currency units (such as cents in i64) or use a dedicated decimal library rather than binary floating-point types.
Special IEEE 754 Values: INFINITY, -0.0, and NaN
Unlike integer arithmetic—where dividing by zero panics—floating-point arithmetic never panics on overflow or division by zero. Instead, it produces special IEEE 754 values:
| Expression | Result | Inspection Method |
|---|---|---|
1.0 / 0.0 or f64::MAX * 2.0 |
inf (f64::INFINITY) |
.is_infinite() / .is_finite() |
-1.0 / 0.0 |
-inf (f64::NEG_INFINITY) |
.is_infinite() && .is_sign_negative() |
0.0 / 0.0 or (-1.0_f64).sqrt() |
NaN (f64::NAN, "Not a Number") |
.is_nan() |
-1.0 / f64::INFINITY |
-0.0 (Negative zero) |
.is_sign_negative() |
fn main() {
let pos_inf = 1.0_f64 / 0.0;
let neg_inf = -1.0_f64 / 0.0;
let nan = 0.0_f64 / 0.0;
let neg_zero = -0.0_f64;
println!("pos_inf: {pos_inf} (is_infinite: {})", pos_inf.is_infinite());
println!("neg_inf: {neg_inf} (is_finite: {})", neg_inf.is_finite());
println!("nan: {nan} (is_nan: {})", nan.is_nan());
println!("-0.0 == 0.0: {} (neg_zero.is_sign_negative: {})", neg_zero == 0.0, neg_zero.is_sign_negative());
}
pos_inf: inf (is_infinite: true)
neg_inf: -inf (is_finite: false)
nan: NaN (is_nan: true)
-0.0 == 0.0: true (neg_zero.is_sign_negative: true)
NaN Comparisons and total_cmp
According to IEEE 754, NaN is unordered and not equal to anything, including itself (f64::NAN == f64::NAN is false). Never test for NaN with x == f64::NAN; always call x.is_nan().
Because NaN != NaN violates the reflexivity law of total equality (a == a), f32 and f64 implement PartialEq and PartialOrd, but do not implement Eq or Ord. Consequently, calling .sort() on a slice of floats does not compile; use .sort_by(f64::total_cmp) instead:
fn main() {
let mut measurements: [f64; 5] = [3.5, f64::NAN, -1.2, 0.0, f64::INFINITY];
// `total_cmp` defines a total ordering: -NaN < -inf < ... < -0.0 < +0.0 < ... < +inf < +NaN
measurements.sort_by(f64::total_cmp);
println!("Sorted: {measurements:?}");
}
Sorted: [-1.2, 0.0, 3.5, inf, NaN]
Built-In Mathematical Methods
f32 and f64 provide rich mathematical methods directly on primitive values, and mathematical constants live in std::f32::consts and std::f64::consts:
fn main() {
let x: f64 = 3.75;
println!("floor: {}, ceil: {}, round: {}, trunc: {}", x.floor(), x.ceil(), x.round(), x.trunc());
println!("sqrt(16.0): {}, cbrt(27.0): {}", 16.0_f64.sqrt(), 27.0_f64.cbrt());
println!("2.0^10: {}, hypot(3, 4): {}", 2.0_f64.powi(10), 3.0_f64.hypot(4.0));
println!("clamp: {}", x.clamp(0.0, 2.5));
}
floor: 3, ceil: 4, round: 4, trunc: 3
sqrt(16.0): 4, cbrt(27.0): 3
2.0^10: 1024, hypot(3, 4): 5
clamp: 2.5
Common Errors
Mixing Integer Literals or f32 with f64
Rust does not implicitly convert integers to floats—even integer literals—nor does it implicitly widen f32 to f64:
// ❌ does not compile
fn main() {
let radius: f64 = 5;
let area = 3.14_f32 * radius;
}
error[E0308]: mismatched types
--> src/main.rs:2:23
|
2 | let radius: f64 = 5;
| --- ^ expected `f64`, found integer
| |
| expected due to this
|
help: use a float literal
|
2 | let radius: f64 = 5.0;
| ++
error[E0277]: cannot multiply `f32` by `f64`
--> src/main.rs:3:25
|
3 | let area = 3.14_f32 * radius;
| ^ no implementation for `f32 * f64`
error: aborting due to 2 previous errors
Write 5.0 for float literals, and convert explicitly (f64::from(single_prec) or int_val as f64) when combining types.