Reputation: 53
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]
is only returned if N == 1
and [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.
Upvotes: 3
Views: 1048
Reputation: 154836
Here is a solution that is not particularly clever, but is easy to understand and resembles the original:
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)
}
}
}
Although the code looks like it checks array sizes at run time, godbolt shows that rustc/LLVM is able to reason that [f64; N].as_slice().try_into()
always succeeds in coercing the array-turned-slice to [f64; N]
. Generated code for test<1>
and test<2>
thus contains no checks or panic, and test<N>
for N>2
just panics unconditionally due to the panic in the catch-all match arm.
Upvotes: 5
Reputation: 98328
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.
Upvotes: 8