DEV Community

Igor Proskurin
Igor Proskurin

Posted on

Generic constant expressions: a future bright side of nightly Rust

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]
}
Enter fullscreen mode Exit fullscreen mode

or even

struct s<const N: usize> {
    value: [i32; N + 1]
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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]: 
   |                          ++++++++++++++++++++++
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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}>()
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)