Summary
Introduce two new prelude traits, TruncateTo<T>
and WidenTo<T>
, that provide a .truncate()
and .widen()
method, to make integer-to-integer casts more expressive and reduce the number of as
casts required.
Motivation
Casting between differently sized integers isn’t optimal. It requires the use of as
, which is a type conversion grab bag: It also allows casting integers to floats, unsigned integers to signed integers, and even a no-op cast to the same integer type.
The recently added From
impls somewhat alleviate the problem since there is now a different way to widen integers. This isn’t as explicit as .widen()
would be, and, like as
, overloaded, since From
and Into
are very generic traits. If this kind of widening conversion is deemed sufficient, only the .truncate()
part of this RFC can be accepted as well (see the alternative listed below).
Introducing two new (mainly internal) traits and putting them into the prelude makes truncation and widening casts, two of the most common uses of as
, more expressive and harder to get wrong.
Detailed design
Add these two traits to libcore
and its prelude:
trait TruncateTo<T> {
fn truncate(&self) -> T;
}
trait WidenTo<T> {
fn widen(&self) -> T;
}
Provide implementations to convert between differently sized unsigned and signed integers:
macro_rules! impl_trunc_widen {
( $big:ty > $small:ty ) => {
impl TruncateTo<$small> for $big {
fn truncate(&self) -> $small {
*self as $small
}
}
impl WidenTo<$big> for $small {
fn widen(&self) -> $big {
*self as $big
}
}
};
}
impl_trunc_widen!(u16 > u8);
impl_trunc_widen!(u32 > u8);
impl_trunc_widen!(u32 > u16);
impl_trunc_widen!(u64 > u8);
impl_trunc_widen!(u64 > u16);
impl_trunc_widen!(u64 > u32);
impl_trunc_widen!(i16 > i8);
impl_trunc_widen!(i32 > i8);
impl_trunc_widen!(i32 > i16);
impl_trunc_widen!(i64 > i8);
impl_trunc_widen!(i64 > i16);
impl_trunc_widen!(i64 > i32);
// Omitted: `i128` and `u128` impls
Note that this intentionally does not allow performing a few casts that can be performed using as
:
- Converting an unsigned to a signed integer or the other way around
- No-op conversion from T to T
- Int-to-float and float-to-int conversions
Conversions to smaller/larger integer types can now be performed by calling .truncate()
and .widen()
:
let lo: u16 = 0xabcdef99u32.truncate();
assert_eq!(lo, 0xef99);
let lo: u8 = 0xabcdef99u32.truncate();
assert_eq!(lo, 0x99);
let lo: u8 = 0x99u32.truncate();
assert_eq!(lo, 0x99);
let wide: u32 = 0x99u8.widen();
assert_eq!(wide, 0x00000099);
Also note that .truncate()
and .widen()
can be stabilized without stabilizing any of the traits. This might be a viable strategy to make this proposal useful to users of stable Rust, even when there are still unresolved questions regarding the traits. This is already used today for the SliceConcatExt
prelude trait.
Drawbacks
- Unlike
as
, a few casts can be performed without specifying the target type.{u16,i16}::truncate
and{u64,i64}::widen
only have a single applicable impl, so they can be called without specifying the target type (resulting in au8,i8,u128,i128
, respectively). Other cases need a type annotation somewhere. This also somewhat precludes adding more impls for these types in the future, since type inference would fail (imagine wideningu128
to a hypotheticalu256
). - Logically, the traits want to use an associated type instead of a type parameter, but the traits need to be implemented multiple times with different resulting types, which isn’t possible if an assoc. type was used.
Alternatives
- A solution to the second drawback would be to only support truncating to the next smallest type (and widening to the next largest), so there’s only one implementation per integer type. The drawback is that casting (for example) a
u32
to au8
requires doingvalue.truncate().truncate()
instead ofvalue.truncate()
with a type annotation. - Just continue using
as
and.into()
. This is what all Rust code uses until now. Introducing a second way to do things makes Rust more complex and harder to learn, and the existingas
casts won’t go away magically, so there will always be two ways to do things (although lintingas
casts where the new methods could be used might be a good idea). - Implement only the
.truncate()
part of this RFC and continue usingas
or preferably.into()
for widening (since.into()
already works for widening today). If Rust gains implicit widening of integers,.widen()
would become obsolete, which can be prevented by not implementing it in the first place.
Unresolved questions
- Should
f32
andf64
implement the traits? What aboutusize
andisize
? - Are the traits useful outside the standard library or do the orphan rules prevent that? Do they make sense for bignum implementations?