Conditional type based on logical comparison to numeric constant?

I'm working on a library that defines a constant, at compile-time, based on an environment variable. The variable declaration is const MAX_SIZE: usize = X;, where X is a user-selected power of 2, e.g. 512, 1024, 2048, etc.

My goal is to conditionally define a type, at compile time, based on the value provided. For example:

  • If MAX_SIZE < u8::MAX, I'd like to compile type OptIdx = Option<u8>.
  • If (u8::MAX < MAX_SIZE) && (MAX_SIZE < u16::MAX), I'd like to instead compile type OptIdx = Option<u16>.

My motivation is to save memory where possible. Consider the below program on a 64-bit system:

fn main() {
    use std::mem::size_of;
    println!("size_of::<Option<u8>>() == {}", size_of::<Option<u8>>());
    println!("size_of::<Option<usize>>() == {}", size_of::<Option<usize>>());
}

It outputs:

size_of::<Option<u8>>() == 2
size_of::<Option<usize>>() == 16

So I can save space by using a a smaller unsigned and cast with as later if necessary to index a collection. Not sure how to do this, conditional compilation doesn't seem to support the logical operator <. This 2018 issue seems related, but did not find a solution within.

Is it possible to conditionally define a type based on comparison to a constant known at compile-time?

Thanks in advance!

Did you look into something like cfg_if? I believe you can provide arbitrary cfg flags using rustc or cargo, (though you might need a build script if trying to translate over from an environment variable. I haven't done it.)

cfg_if::cfg_if! {
    if #[cfg(max_size = "256")] {
        type Inner = u8;
        const MAX_SIZE: usize = u8::MAX;
    } else {
        type Inner = usize;
        const MAX_SIZE: usize = usize::MAX;
        
    }
}

fn main() {
    use std::mem::size_of;
    println!("size_of::<Option<u8>>() == {}", size_of::<Option<u8>>());
    println!("size_of::<Option<usize>>() == {}", size_of::<Option<usize>>());
    println!("size_of::<Option<Inner>>() == {}", size_of::<Option<Inner>>());
}

EDIT: Ah, I just saw that you wanted to support non-MAX values. This might be too much then. You might need to write a macro to generate the cfg_if chain.

Thanks, I didn't know about cfg_if! Looks like only #[cfg(...)] expressions are supported in the ifs, so I indeed can't use logical operators to match non-MAX values to the smallest primitive.

But a macro like this one might be part of the solution! Since I could use <, <=, etc inside the macro.

I recommend using a helper trait generic over "inequality bounds" to then allow to map such bounds to different types:

const MAX_SIZE: usize = 256;

type Index = optimized_index_for_size!(MAX_SIZE);

fn main ()
{
    use ::core::mem::size_of;
    use Option as Opt;

    dbg!(
        size_of::<Opt< Index >>(), // 4
        size_of::<Opt< optimized_index_for_size!(255) >>(), // 2
        size_of::<Opt< optimized_index_for_size!(500) >>(), // 4
        size_of::<Opt< optimized_index_for_size!(70_000) >>(), // 8
    );
}

// where:
macro_rules! optimized_index_for_size {( $MAX_SIZE:expr $(,)? ) => (
    <
        ()
        as
        $crate::__Helper<
            { $MAX_SIZE <= (::core::u8 ::MAX as usize) },
            { $MAX_SIZE <= (::core::u16::MAX as usize) },
            { $MAX_SIZE <= (::core::u32::MAX as usize) },
        >
    >::Idx
)} pub(crate) use optimized_index_for_size;
2 Likes

That solution is perfect for my needs, thank you!