genericsrusttypes

Is it possible to select the smallest integer type based on a const generic value in Rust?


I'm working on a library where I want to select the smallest unsigned integer type based on a const generic value at compile time.

For example, I want to define something like this:

type T = MinType<255>::Type;  // T should be u8
type T = MinType<256>::Type;  // T should be u16
type T = MinType<70000>::Type; // T should be u32

In other words, I'd like to define a generic helper (e.g., a trait or struct) such that:

MinType::<N>::Type

expands to the smallest unsigned integer type that can hold all values from 0 to N, for any given const N: usize.

Is this kind of type selection possible in stable or nightly Rust? Or does it require macros, nightly features, or external crates?


Solution

  • With #![feature(generic_const_exprs)] it's easily achievable:

    #![feature(generic_const_exprs)]
    struct MyType<const N: u128>;
    
    trait ConstGenericType {
        type Type;
    }
    
    struct MyLogType<const N: u128>;
    
    impl ConstGenericType for MyLogType<8> {
        type Type = u8;
    }
    impl ConstGenericType for MyLogType<16> {
        type Type = u16;
    }
    impl ConstGenericType for MyLogType<32> {
        type Type = u32;
    }
    impl ConstGenericType for MyLogType<64> {
        type Type = u64;
    }
    impl ConstGenericType for MyLogType<128> {
        type Type = u128;
    }
    
    impl<const N: u128> ConstGenericType for MyType<N>
    where
        MyLogType<{ required_bits(N) }>: ConstGenericType,
    {
        type Type = <MyLogType<{ required_bits(N) }> as ConstGenericType>::Type;
    }
    
    const fn required_bits(n: u128) -> u128 {
        if n < (1 << 8) {
            8
        } else if n < (1 << 16) {
            16
        } else if n < (1 << 32) {
            32
        } else if n < (1 << 64) {
            64
        } else {
            128
        }
    }
    
    type T8 = <MyType<{ u8::MAX as u128 }> as ConstGenericType>::Type;
    type T16 = <MyType<{ u16::MAX as u128 }> as ConstGenericType>::Type;
    type T32 = <MyType<{ u32::MAX as u128 }> as ConstGenericType>::Type;
    type T64 = <MyType<{ u64::MAX as u128 }> as ConstGenericType>::Type;
    type Tword = <MyType<{ usize::MAX as u128 }> as ConstGenericType>::Type;
    type T128 = <MyType<{ u128::MAX }> as ConstGenericType>::Type;
    fn main() {
        dbg!(std::any::type_name::<T8>());
        dbg!(std::any::type_name::<T16>());
        dbg!(std::any::type_name::<T32>());
        dbg!(std::any::type_name::<T64>());
        dbg!(std::any::type_name::<Tword>());
        dbg!(std::any::type_name::<T128>());
    }
    

    Playground

    The trait is necesseary for an associated type and to be able to declare a bound that enforces the existence for a ::Type on the select values 8, 16, 32, 64 and 128, with an inherent implementation the compiler can't see that it's actually available for all return values of required_bits. generic_const_exprs is required to convert the const generic to the number of bytes required. You could use a macro to add the ConstGenericType implementations all at once.