Summary
Add explicitly-named standard library APIs
for conversion between primitive number types with various semantics:
truncating, saturating, rounding, etc.
This RFC does not attempt to define general-purpose traits
that are intended to be implemented by non-primitive types,
or to support code that wants to be generic over number types.
Motivation
Status quo as of Rust 1.39
The as keyword allows converting between any two of Rust’s primitive number types:
u8
u16
u32
u64
u128
i8
i16
i32
i64
i128
usize
isize
f32
f64
However the semantics of that conversion varies based on the combination of input and output type.
The Rustonomicon documents:
- casting between two integers of the same size (e.g. i32 -> u32) is a no-op
- casting from a larger integer to a smaller integer (e.g. u32 -> u8) will
truncate
- casting from a smaller integer to a larger integer (e.g. u8 -> u32) will
- zero-extend if the source is unsigned
- sign-extend if the source is signed
- casting from a float to an integer will round the float towards zero
- casting from an integer to float will produce the floating point
representation of the integer, rounded if necessary (rounding to
nearest, ties to even)
- casting from an f32 to an f64 is perfect and lossless
- casting from an f64 to an f32 will produce the closest possible value
(rounding to nearest, ties to even)
(Note: the proposed fix for the float to integer case is to make the conversion saturating.)
Additionally, the general-purpose From trait
(and therefore TryFrom through the blanket impl<T, U> TryFrom<U> for T where U: Into<T>)
is implemented in cases where the conversion is exact:
when every value of the input type is converted to a distinct value of the output type
that represents exactly the same real number.
The TryFrom trait is also implemented for the remaining combinations of integer types,
returning an error when the input value is outside of the MIN..=MAX range
supported by the output type.
For this purpose usize and isize are conservatively considered to be
potentially any size of at least 16 bits,
to avoid having non-portable From impls that only exist on some platforms.
The table below exhaustively lists those impls,
with F indicating a From impl and TF indicating (only) TryFrom.
Rows are input types, columns outputs.
| ↬ |
u8 |
u16 |
u32 |
u64 |
u128 |
i8 |
i16 |
i32 |
i64 |
i128 |
usize |
isize |
f32 |
f64 |
| u8 |
F |
F |
F |
F |
F |
TF |
F |
F |
F |
F |
F |
F |
F |
F |
| u16 |
TF |
F |
F |
F |
F |
TF |
TF |
F |
F |
F |
F |
TF |
F |
F |
| u32 |
TF |
TF |
F |
F |
F |
TF |
TF |
TF |
F |
F |
TF |
TF |
|
F |
| u64 |
TF |
TF |
TF |
F |
F |
TF |
TF |
TF |
TF |
F |
TF |
TF |
|
|
| u128 |
TF |
TF |
TF |
TF |
F |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
|
|
| i8 |
TF |
TF |
TF |
TF |
TF |
F |
F |
F |
F |
F |
TF |
F |
F |
F |
| i16 |
TF |
TF |
TF |
TF |
TF |
TF |
F |
F |
F |
F |
TF |
F |
F |
F |
| i32 |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
F |
F |
F |
TF |
TF |
|
F |
| i64 |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
F |
F |
TF |
TF |
|
|
| i128 |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
F |
TF |
TF |
|
|
| usize |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
F |
TF |
|
|
| isize |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
TF |
F |
|
|
| f32 |
|
|
|
|
|
|
|
|
|
|
|
|
F |
F |
| f64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
F |
Preferring explicit semantics
When looking at code with a $expr as $ty cast expression,
the semantics of the conversion are often not obvious to human readers.
Deducing the type of the input expression usually requires looking at other parts of the code,
possibly distant ones.
In some cases it’s even possible to make the compiler infer the output type,
with syntax like foo() as _.
It’s also possible for those types to change when a possibly-distant part of the code is modified.
A cast that was previously exact could suddenly have truncation semantics,
which might be incorrect for a given algorithm.
To avoid this, it’s preferable to use for example an explicit u32::from(foo) call
instead of casting with as.
In fact Clippy has a lint
for exactly this (though a silent by default one).
In some other cases however,
truncation or some other conversion semantics might be the desired behavior.
Communicating that intent to human readers is just as useful then as it would be with a from call.
(Not yet) deprecating the as keyword
Because of the ambiguity described above, deprecating as casts entirely has been
discussed before.
Providing an alternative with something like this RFC would be a prerequisite,
but this RFC is not proposing such a deprecation.
Guide-level explanation
For the purpose of conversion semantics, Rust has two kinds of primitive number types:
floating point and integer.
This makes four combinations of input and output kind.
For a given conversion let’s call:
I the input type
i the input value
O the output type
o the output value, the result of the conversion: let o: O = convert(i: I);
Exact conversions
For combinations of primitive number types where they are implemented,
the general-purpose convert::Into and convert::From traits offer exact conversion:
o always represents the exact same real number as i.
The I::into(self) -> O method and I::from(O) -> Self constructor are available
without importing the corresponding trait explicitly, since the traits are in the prelude.
Integer to integer
For all combinations of primitive integer types I and O,
the standard library additionally provides:
-
The I::try_into<O>(self) -> Result<O, E> method
and O::try_from<I>(I) -> Result<Self, E> constructor
for fallible conversion.
These are inherent methods of primitive integers
that delegate to the general-purpose convert::Into and convert::From traits.
Although these traits are not in the prelude,
they do not need to be in scope for the inherent methods to be called.
This returns an error when i is outside of the range that O can represent.
The error type E is either convert::Infallible (where a From is also implemented)
or num::TryFromIntError.
-
The I::modulo_to<O>(self) -> O I::wrapping_to<O>(self) -> O method for wrapping conversion,
also known as bit-truncating conversion.
In terms of arithmetic, o is the only value that O can represent
such that o = i + k×2ⁿ where k is an integer and n is the number of bits of O.
In terms of memory representation, this takes the n lower bits of the input value.
The upper bits are truncated off.
This is an a sense opposite of float-to-integer truncation where the less-significant
fractional part is truncated off.
For example, 0xCAFE_u16 maps to 0xFE_u8, and 130_u32 to -126_i8.
Note: This is the behavior of the as operator.
-
The I::saturating_to<O>(self) -> O method for saturating conversion.
o is the value arithmetically closest to i that O can represent.
This is O::MIN or O::MAX for underflow or overflow respectively.
Float to float, integer to float
For all combinations of primitive number types I (floating point or integer)
and primitive floating point type O,
the standard library additionally provides:
-
I::round_to<O>(self) -> O I::approx_to<O>(self) -> O for approximate conversion.
o is the value arithmetically closest to i that O can represent.
Overflow produces infinity of the same sign as i.
For floating point I, rounding may happen due to precision loss through fewer mantissa bits.
For integer I, rounding may happen for large values (positive or negative).
Rounding is according to roundTiesToEven mode as defined in IEEE 754-2008 §4.3.1:
pick the nearest floating point number, preferring the one with an even least significant digit
if exactly halfway between two floating point numbers.
Note: This is the behavior of the as operator.
Float to integer
For all combinations of primitive floating point type I and primitive integer type O,
the standard library additionally provides:
-
I::saturating_to<O>(self) -> O for saturating truncating conversion.
The fractional part of i is truncated off in order to keep the integral part.
That is, the value is rounded towards zero.
Underflow maps to O::MIN.
Overflow maps to O::MAX.
NaN maps zero.
Note: this may become
the behavior of the as operator in a future Rust version.
-
I::unchecked_to<O>(self) -> O for unsafe truncating conversion.
The fractional part of i is truncated off in order to keep the integral part.
That is, the value is rounded towards zero.
This method is an unsafe fn.
It has Undefined Behavior if i is infinite, is NaN,
or cannot be represented exactly in O after truncation.
Note: This is the behavior of the as operator as of Rust 1.39,
even though it can be used outside of any unsafe block or function.
Reference-level explanation
Everything discussed in this RFC is defined in the core crate and reexported in the std crate.
Exact, fallible, and unsafe truncating conversion conversions described above
already exist in the standard library.
FIXME: this assumes PR #66852
and PR #66841 are accepted and have landed.
Inherent methods are added that delegate calls to the corresponding trait method.
They are generic to support multiple return types.
Some of these impls are macro-generated, to reduce source code duplication:
impl $Int {
// Added in https://github.com/rust-lang/rust/pull/66852
pub fn try_from<T>(value: T) -> Result<Self, Self::Error>
where Self: TryFrom<T> { /* … */}
pub fn try_into<T>(self) -> Result<T, Self::Error>
where Self: TryInto<T> { /* … */}
pub fn wrapping_to<T>(self) -> T where Self: IntToInt<T> { /* … */}
pub fn saturating_to<T>(self) -> T where Self: IntToInt<T> { /* … */}
pub fn approx_to<T>(self) -> T where Self: IntToFloat<T> { /* … */}
}
impl $Float {
pub fn approx_to<T>(self) -> T where Self: FloatToFloat<T> { /* … */}
pub fn saturating_to<T>(self) -> T where Self: FloatToInt<T> { /* … */}
// Added in https://github.com/rust-lang/rust/pull/66841
pub unsafe fn unchecked_to<T>(self) -> T where Self: FloatToInt<T> { /* … */}
}
Four supporting traits are added to the convert module:
mod private {
pub trait Sealed {}
}
pub trait IntToInt<T>: self::private::Sealed {
// Supporting methods…
}
pub trait IntToFloat<T>: self::private::Sealed {
// Supporting methods…
}
pub trait FloatToFloat<T>: self::private::Sealed {
// Supporting methods…
}
pub trait FloatToInt<T>: self::private::Sealed {
// Supporting methods…
}
Each trait has methods with the same signatures as inherent methods that delegate calls to them.
The sealed trait pattern is used to prevent impls outside of the standard library.
This will allow adding more methods after the traits are stabilized.
See Future possibilities below.
The traits are implemented for all relevant combinations of types.
Again, some of these impls are macro-generated:
impl IntToInt<$OutputInt> for $InputInt { /* … */ }
impl IntToFloat<$OutputFloat> for $InputInt { /* … */ }
impl FloatToFloat<$OutputFloat> for $InputFloat { /* … */ }
impl FloatToInt<$OutputInt> for $InputFloat { /* … */ }
Drawbacks
This adds a significant number of items to libcore.
However primitive number types already have numerous inherent methods
and trait methods, so this isn’t unprecedented.
If the as keyword is never deprecated or until it is,
we would in many cases have two ways of doing the same thing.
Rationale and alternatives
The “shape” of the API could be different.
Namely, instead of inherent methods that delegate to supporting traits we could have:
-
Plain trait methods, with traits that need to be imported into scope.
This less convenient to users.
-
Plain trait methods, with traits in the prelude.
The bar is generally high to add anything to the prelude.
-
Non-generic inherent methods that include the name name of the return type in their name:
wrapping_to_u8, wrapping_to_i8, wrapping_to_u16, …
This causes multiplicative explosion of the number of new items.
This RFC however makes no active attempt at supporting callers who are themselves generic
to support multiple number types.
Traits are only used as a way to avoid multiplicative explosion.
This RFC proposes adding multiple conversions methods with various semantics
even for combinations of types where they are “useless” because the conversion is always exact.
For example, u8::wrapping_to<i32> and u8::saturating_to<i32>
both behave the same as <u8 as Into<i32>>::into.
This avoids the question of what to do about the portability of impls for usize and isize.
In the case of float to float conversion specifically,
I = f64 and O = f32 is the only combination that is really useful.
We could have only f64::approx_to(self) -> f32 instead of generic methods with a trait.
Keeping a trait anyway makes this more consistent with the other kinds of conversions,
and is compatible with a future addition of new primitives floating point types (f16, f80, …)
in case those are ever desired.
Prior art
FIXME
Unresolved questions
FIXME
Future possibilities
This pattern of API is extensible
and supports adding more methods with different conversion semantics.
For example:
-
Wrapping approximate floating point to integer conversion
that “wraps around” instead of saturating.
(But what to do about infinities and NaN?)
-
Fallible approximate floating point to floating point conversion
that returns an error instead of mapping a finite value to infinity
-
Fallible approximate floating point to integer conversion
that returns an error for NaN and instead of saturating to MAX or MIN.
-
Fallible exact conversion
that never rounds and returns an error if the input value
doesn’t have an exact representation in the output type,
for some subset or all of:
- Integer to floating point
- Floating point to integer
- Floating point to floating point
This RFC doesn’t explore which of these (or others)
are useful enough to merit adding to the standard library.