Search code examples
genericsrustconstants

How can I ensure constant evaluation when using generic consts?


Attempting to evaluate two generic constants into a new const is denied by the compiler. Here I'm trying to convert between generic variants of Foo:

struct Foo<const X: isize> {
    n: isize,
}

impl<const A: isize> Foo<A> {
    fn convert<const B: isize>(self) -> Foo<B> {
        // arbitrary operation with A and B into constant
        const n: isize = A & B;
        
        Foo { n }
    }
}
error[E0401]: can't use generic parameters from outer item
 --> src/lib.rs:8:26
  |
5 | impl<const A: isize> Foo<A> {
  |            - const parameter from outer item
...
8 |         const n: isize = A & B;
  |                          ^ use of generic parameter from outer item
  |
  = note: a `const` is a separate item from the item that contains it

error[E0401]: can't use generic parameters from outer item
 --> src/lib.rs:8:30
  |
6 |     fn convert<const B: isize>(self) -> Foo<B> {
  |                      - const parameter from outer item
7 |         // arbitrary operation with A and B into constant
8 |         const n: isize = A & B;
  |                              ^ use of generic parameter from outer item
  |
  = note: a `const` is a separate item from the item that contains it

Rewriting it to avoid the const will compile:

impl<const A: isize> Foo<A> {
    fn convert<const B: isize>(self) -> Foo<B> {
        Foo { n: A & B }
    }
}

However, how can I be sure this isn't evaluated at run-time? Sometimes it is critical for performance that calculations are done at compile-time. I feel it is important to get the guarantee from a const.


Solution

  • You cannot create a parameterized const item. Here is a smaller example and its error:

    fn foo<const A: isize, const B: isize>() {
        const C: isize = A & B;
    }
    
    error[E0401]: can't use generic parameters from outer item
     --> src/lib.rs:2:22
      |
    1 | fn foo<const A: isize, const B: isize>() {
      |              - const parameter from outer item
    2 |     const C: isize = A & B;
      |                      ^ use of generic parameter from outer item
      |
      = note: a `const` is a separate item from the item that contains it
    

    The key part of the error to understand is "a const is a separate item from the item that contains it" which means the way it is organized by the compiler is as-if it were like this:

    const C: isize = ...;
    
    fn foo<const A: isize, const B: isize>() {
       ...
    }
    

    With the further understanding that C must be the same for all instances of foo which means it's value can't depend on A and B. This is just how const items are defined currently.


    That all being said, in your code you don't need a constant since it is only used to initialize a runtime value. So things like this will work just fine:

    fn foo<const A: isize, const B: isize>() {
        let c: isize = A & B;
    }
    

    This will almost definitely not incur a runtime cost. Generic parameters (that aren't lifetimes) compile multiple instances of the function separately. If you were to call this function as foo::<1, 1>() and foo::<2, 2>() the code that is generated will be the same as this:

    fn foo_1_1() {
        let c: isize = 1 & 1;
    }
    
    fn foo_2_2() {
        let c: isize = 2 & 2;
    }
    

    I specifically say "almost definitely" because constant-evaluation done via optimization passes is not guaranteed by the compiler, however this is a trivial evaluation that would be surprising if it didn't. You can always verify by inspecting the generated assembly.


    If you are still concerned about a runtime cost, you can still get a guarantee of constant evaluation by using an inline const {} block (Rust 1.79):

    fn foo<const A: isize, const B: isize>() {
        let c: isize = const { A & B };
    }
    

    This has the added benefit of emitting a compiler error if the expression contains a value or expression that is not a constant. This does not have the same limitation as on const items.

    If you need this same behavior before 1.79, it can also be accomplished with associated const items. Those can use A and B and will ensure that the value was created at compile-time. You just need to make a dummy type to house it:

    fn foo<const A: isize, const B: isize>() {
        struct Const<const A: isize, const B: isize>;
        impl<const A: isize, const B: isize> Const<A, B> {
            const CONST: isize = A & B;
        }
    
        let c: isize = Const::<A, B>::CONST;
    }
    

    As a final side note, if you do need an evaluation of A and B that requires a constant context besides a const item, like creating another generic parameter:

    struct Foo<const T: isize> {}
    
    fn foo<const A: isize, const B: isize>() -> Foo<{A & B}> {
        Foo {}
    }
    

    Then this is currently an error since generic_const_exprs have not yet been stabilized.

    error: generic parameters may not be used in const operations
     --> src/lib.rs:3:50
      |
    3 | fn foo<const A: isize, const B: isize>() -> Foo<{A & B}> {
      |                                                  ^ cannot perform const operation using `A`
      |
      = help: const parameters may only be used as standalone arguments, i.e. `A`
      = help: add `#![feature(generic_const_exprs)]` to allow generic const expressions
    

    This use may be stabilized in the future and you can currently try it the nightly toolchain, but be warned that it is incomplete and you may encounter weird errors, compiler bugs, or undefined behavior particularly in more complicated scenarios.