I was wondering what would happen when I cast a very large float value to an integer. This is an example I wrote:
fn main() {
let x = 82747650246702476024762_f32;//-1_i16;
let y = x as u8;
let z = x as i32;
println!("{} {} {}", x, y, z);
}
and the output is:
$ ./casts
82747650000000000000000 0 -2147483648
Obviously the float wouldn't fit in any of the integers, but since Rust so strongly advertises that it is safe, I would have expected an error of some kind. These operations use the llvm fptosi
and fptoui
instructions, which produce a so called poison value if the value doesn't fit within the type it has been casted to. This may produce undefined behavior, which is very bad, especially when writing Rust code.
How can I be sure my float to int casts don't result in undefined behavior in Rust? And why would Rust even allow this (as it is known for creating safe code)?
Casting a float to an integer truncates the value, so if you have 3.999998 , and you cast it to an integer , you get 3 .
A float value can be converted to an int value no larger than the input by using the math. floor() function, whereas it can also be converted to an int value which is the smallest integer greater than the input using math.
Converting Floats to Integers Python also has a built-in function to convert floats to integers: int() . In this case, 390.8 will be converted to 390 . When converting floats to integers with the int() function, Python cuts off the decimal and remaining numbers of a float to create an integer.
Casting in python is therefore done using constructor functions: int() - constructs an integer number from an integer literal, a float literal (by removing all decimals), or a string literal (providing the string represents a whole number)
In Rust 1.44 and earlier, if you use as
to cast a floating-point number to an integer type and the floating-point number does not fit¹ in the target type, the result is an undefined value², and most things that you can do with it cause undefined behavior.
This serious issue (#10184) was fixed in Rust 1.45. Since that release, float-to-integer casts saturate instead (that is, values that are too large or small are converted to T::MAX
or T::MIN
, respectively; NaN is converted to 0).
In older versions of Rust, you can enable the new, safe behavior with the -Z saturating-float-casts
flag. Note that saturating casts may be slightly slower since they have to check the type bounds first. If you really need to avoid the check, the standard library provides to_int_unchecked
. Since the behavior is undefined when the number is out of range, you must use unsafe
.
(There used to be a similar issue for certain integer-to-float casts, but it was resolved by making such casts always saturating. This change was not considered a performance regression and there is no way to opt in to the old behavior.)
¹ "Fit" here means either NaN, or a number of such large magnitude that it cannot be approximated by the smaller type. 8.7654321_f64
will still be truncated to 8
by an as u8
cast, even though the value cannot be represented exactly by the destination type -- loss of precision does not cause undefined behavior, only being out of range does.
² A "poison" value in LLVM, as you correctly note in the question, but Rust itself does not distinguish between undef
and poison values.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With