Generic constant expressions: a future bright side of nightly Rust
Igor Proskurin
Posted on May 17, 2024
When I just started looking into generic programming in Rust, I was quite annoyed that something that is trivially done with C++ templates, cannot be done in Rust. You just can't... Generic traits are pretty cool, and certainly you can use const
parameters in generics but if you just try
fn f<const N: usize>() -> [i32; 2 * N] {
[2; 2 * N]
}
or even
struct s<const N: usize> {
value: [i32; N + 1]
}
in both cases Ferris would be upset and we get error: generic parameters may not be used in const operations
. So how can we implement something generic that is allocated on the stack or should we ask the operating system each time for memory on the heap?
Generic constant expression parameters
Unfortunately, there is no way of doing it in stable Rust. And we have to opt to a nightly build. Let's try it! I will use the latest nightly-x86_64-pc-windows-msvc
. However, this feature seems to be here for while, and this may compile in other versions as well. Or maybe not... It looks like support of generic constant expressions is still highly experimental.
First look is into The Unstable Book. Well, it does not look informative but gives us some background from the rust-lang Github project-const-generics. It says:
The implementation is still far from ready but already available for experimentation.
Nice! That's exactly what we want...
Also, some interesting discussions and difficulties with implementing safe generic constant expressions can be found here and here.
Constant expression generic parameters in functions
Gear up and embrace for impact: rustup override set nightly
, and we are in uncharted waters of experimental Rust.
Let's try again:
#![feature(generic_const_exprs)]
#![allow(incomplete_features)]
fn f<const N: usize>() -> [i32; 2 * N] {
[2; 2 * N]
}
Now it compiles and let v = f::<2>()
produces what we asked for [2, 2, 2, 2]
.
Constant expression generic parameters in types
Let's try a generic struct that wraps an array of a size known at compile time that is a constant expression.
#[derive(Debug)]
struct s<const N: usize>
//where [i32; 2* N + 1]:
{
value: [i32; 2 * N + 1]
}
Oops, this does not compile:
error: unconstrained generic constant
--> src/main.rs:11:12
|
11 | value: [i32; 2 * N + 1]
| ^^^^^^^^^^^^^^^^
|
help: try adding a `where` bound
|
8 | struct s<const N: usize> where [(); 2 * N + 1]:
| ++++++++++++++++++++++
The problem here, as far as I understand from the discussion, is with the const-well-formdness. That is having a constant parameter N
, how to verify that 2 * N + 1
is well-formed and won't, for example, overflow? So we need to add a bound.
We currently use where [(); expr]: as a way to add additional const wf bounds. Once we have started experimenting with this it is probably worth it to add a more intuitive way to add const wf bounds.
Adding this bound certainly helps, and now this compiles:
#![feature(generic_const_exprs)]
#![allow(incomplete_features)]
#[derive(Debug)]
struct s<const N: usize>
where [(); 2 * N + 1]:
{
value: [i32; 2 * N + 1]
}
fn main() {
let v: s::<2> = s {value: [1, 2, 3, 4, 5]};
println!("{:?}", v);
}
and gives s { value: [1, 2, 3, 4, 5] }
. And if we replace this declaration with a wrong one let v: s::<3> = s {value: [1, 2, 3, 4, 5]}
, it errors out with a meaningful error message
error[E0308]: mismatched types
--> src/main.rs:12:31
|
12 | let v: s::<3> = s {value: [1, 2, 3, 4, 5]};
| ^^^^^^^^^^^^^^^ expected `7`, found `5`
|
= note: expected constant `7`
found constant `5`
Good!
What does not work...
It is not possible to define a recursive invocation of a function with a constant expression parameter like this (which is again trivial in C++ template metaprogramming):
fn factorial<const N: usize>() -> usize
where [(); N - 1] {
// ???
factorial::<{N-1}>()
}
The problems is (1) how to stop the recursion, and (2) how to impose recursive generic-parameter bounds that the compiler asks us for.
Declaring internal constant expression parameters does not work either:
fn f<const N: usize>() -> [i32; 2 * N] {
const M: usize = 2 * N;
[2; 2 * N]
}
error[E0401]: can't use generic parameters from outer item
--> src/main.rs:10:26
|
9 | fn f<const N: usize>() -> [i32; 2 * N] {
| - const parameter from outer item
10 | const M: usize = 2 * N;
| ^ use of generic parameter from outer item
|
= note: a `const` is a separate item from the item that contains it
Summary
Let's hope that generic constant expressions will find their way in future safe and stable Rust. It will certainly help the expressiveness of the language when it comes to implementing generic libraries with static-sized aggregate types known at compile time.
Posted on May 17, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.