简体   繁体   中英

Rust: different return values dependent on value of generic constant

Let's consider a function like this:

fn test<const N: usize>() -> [f64; N] {
    if N == 1 {
        [0.0_f64; 1]
    } else if N == 2 {
        [1.0_f64; 2]
    } else {
        panic!()
    }
}

My understanding is that the compiler would evaluate the value of N at compile time. If this is the case, the if statement could also be evaluated at compile time, thus the right type should be returned since [0.0_f64; 1] [0.0_f64; 1] is only returned if N == 1 and [1.0_f64; 2] [1.0_f64; 2] is only returned if N == 2 .

Now, when i try to compile this code, the compiler fails, basically telling me that the dimensions of the returned arrays are wrong since they do not explicitly have N as length.

I do realize, that i could implement this specific example as

fn test<const N: usize>() -> [f64; N] {
    match N {
        1 => { [0.0_f64; N] },
        2 => { [1.0_f64; N] },
        _ => { panic!("Invalid value {}", N) },
    }
}

But that does not work in my actual code, since that uses different functions with fixed array sizes for the different branches.

Is there a way to do this at all? Maybe using something like the #![cfg] makro?

To clarify my why my problem does not work, let's write this out:

fn some_fct() -> [f64; 1] {
    [0.0_f64; 1]
}
fn some_other_fct() -> [f64; 2] {
    [1.0_f64; 2]
}

fn test<const N: usize>() -> [f64; N] {
    match N {
        1 => some_fct(),
        2 => some_other_fct(),
        _ => {
            panic!("Invalid value {}", N)
        }
    }
}

And I cannot really write some_fct() and some_other_fct() to return with generic sizes due to other restrictions in the program structure.

You can do that with a generic trait:

trait Test<const N: usize> {
    fn test() -> [f64; N];
}

Then you implement it for a zero sized type:

struct T;

impl Test<1> for T {
    fn test() -> [f64; 1] {
        return [0.0_f64; 1];
    }
}

impl Test<2> for T {
    fn test() -> [f64; 2] {
        return [1.0_f64; 2];
    }
}

The drawback is that calling it is a bit cumbersome:

fn main() {
    dbg!(<T as Test<1>>::test());
    dbg!(<T as Test<2>>::test());
}

But as @eggyal comments below, you can add a generic function with a well-written bound to get your required syntax:

fn test<const N: usize>() -> [f64; N]
where
    T: Test<N>
{
    T::test()
}
fn main() {
    dbg!(test::<1>());
    dbg!(test::<2>());
}

Now, you don't have the behavior of " panic! when a wrong N is used". Consider that a feature instead of a limitation: if you use a wrong N your code will fail to compile instead of panic at runtime.

If you really want the panic!() behavior you could get it using the unstable feature of #![feature(specialization)] , just adding default to this impl:

impl<const N: usize> Test<N> for T {
    default fn test() -> [f64; N] {
        panic!();
    }
}

But that feature is explicitly marked as incomplete, so I would not count on it, yet.

Here is a solution that is not very clever, but is easy to read and straightforward:

fn test<const N: usize>() -> [f64; N] {
    match N {
        1 => some_fct().as_slice().try_into().unwrap(),
        2 => some_other_fct().as_slice().try_into().unwrap(),
        _ => {
            panic!("Invalid value {}", N)
        }
    }
}

Looking at godbolt , rustc/LLVM is perfectly able to reason that as_slice().try_into().unwrap() will never panic and generate optimal code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM