- Feature Name: numeric-conversions
- Start Date: 2019-10-30
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
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
- NOTE: currently this will cause Undefined Behavior if the rounded value cannot be represented by the target integer type. This includes Inf and NaN. This is a bug and will be fixed.
- 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 typei
the input valueO
the output typeo
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 andO::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
andconvert::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 thatO
can represent. The error typeE
is eitherconvert::Infallible
(where aFrom
is also implemented) ornum::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 thatO
can represent such thato = i + k×2ⁿ
wherek
is an integer andn
is the number of bits ofO
.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 to0xFE_u8
, and130_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 toi
thatO
can represent. This isO::MIN
orO::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 toi
thatO
can represent. Overflow produces infinity of the same sign asi
.For floating point
I
, rounding may happen due to precision loss through fewer mantissa bits. For integerI
, 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 toO::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 ifi
is infinite, isNaN
, or cannot be represented exactly inO
after truncation.Note: This is the behavior of the
as
operator as of Rust 1.39, even though it can be used outside of anyunsafe
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 impl
s 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 impl
s 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 impl
s 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 toMAX
orMIN
. -
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.