I'm hitting "generic parameters may not be used in const operations" and "can't use generic parameters from outer item" way too often lately.
My primary use case is parametrizing a function with const generics, which are then used to define the size of arrays on the stack.
For example this:
fn compute<const N: usize>() {
let arr = [0u8; N * 3];
}
Produces:
error: generic parameters may not be used in const operations
--> src/main.rs:2:21
|
2 | let arr = [0u8; N * 3];
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`
Sometimes I need to reuse the value:
fn compute<const N: usize>() {
const SIZE: usize = N * 3;
let arr = [0u8; SIZE];
}
Produces:
error[E0401]: can't use generic parameters from outer item
--> src/main.rs:2:25
|
1 | fn compute<const N: usize>() {
| - const parameter from outer item
2 | const SIZE: usize = N * 3;
| ^ use of generic parameter from outer item
|
= note: a `const` is a separate item from the item that contains it
I looked for issues, but have not really found which one allows for things like this.
The only workaround that works is to offload the burden and correctness guarantees onto the caller:
#![feature(generic_const_exprs)]
fn compute<const N: usize>() {
let arr = [0u8; N * 3];
}
produces
warning: the feature `generic_const_exprs` is incomplete and may not be safe to use and/or cause compiler crashes
--> src/lib.rs:1:12
|
1 | #![feature(generic_const_exprs)]
| ^^^^^^^^^^^^^^^^^^^
|
= note: see issue #76560 <https://github.com/rust-lang/rust/issues/76560> for more information
= note: `#[warn(incomplete_features)]` on by default
error: unconstrained generic constant
--> src/lib.rs:3:21
|
3 | let arr = [0u8; N * 3];
| ^^^^^
|
help: try adding a `where` bound
|
2 | fn compute<const N: usize>() where [(); N * 3]: {
| ++++++++++++++++++
If you also add the suggested where bound it compiles.
Indeed, and I actually use it in some cases already quite heavily.
But it still doesn't allow the second option with a constant, which I also need fairly often. I recall reading somewhere that it is a problem that constant may end up in the binary duplicated, but these constants are inaccessible from the outside, so maybe there is a way to create a unique instance of it for every generic variant?
#![feature(generic_const_exprs, generic_const_items)]
const SIZE<const N: usize>: usize = N * 3;
fn compute<const N: usize>() where [(); SIZE::<N>]: {
let arr = [0u8; SIZE::<N>];
}
works. Note that const items just like functions capture neither type parameters nor const generics, even if they are inside another function. As such you need to add the const generic to the item itself. And then because generic_const_exprs needs the where bound to refer to the const item, you have to pull the const item out of the compute function.
This is interesting, I didn't know about generic_const_items existence until now. I actually used const fn for the same purpose before, but I guess this is a bit more idiomatic.
But it still suffers from the same usability issue: one can't define a constant inside the function with the result of SIZE::<N> and reuse it conveniently, something like this:
I understand that they do not today. The way I think about it as Rust user is that for every instantiation of the generic function, all constants are copy-pasted into the function and effectively no "environment" exists since these are not runtime variables.
Conceptually, compiler at some point must know all the constants and be able to rewrite the code in exactly the same way as a human would if they wrote a specialized version of each function manually. If a user did that, then those constants would not capture any environment, hence conceptually it should be possible to treat them exactly the same way here too.
So if I have something like this:
fn compute::<const N: usize>() -> usize {
const X: usize = N * 3;
X
}
compute::<2>();
Compiler would rewrite it to something equivalent to this:
And similarly for any other instantiation of N. I understand that there are probably technical reasons why this is difficult, but I don't see why it can't fundamentally be done.
It would be inconsistent with all other kinds of items that you can put inside a function. Functions, statics, types (and even modules that we for some reason allow inside functions) don't capture any generic arguments from the enclosing function.
I see no reason why they should not be able to capture such constants either.
Anything that can capture regular constants, should be able to capture generic constants too since it is essentially a DRY mechanism. Simply (yeah, easier said than done) start from the most outer layer and recursively evaluate generic constants all the way down. Net result will be the same as if there were no generic constants at all.
I have hit a similar-ish but with generic parameters.
// I wanted to write code like this quite often.
trait DoAThing {
fn whatever(self);
}
fn takes_a_thing_doer(_: impl DoAThing) {}
fn foo<T: DoAThing>(t: T) {
struct Foo {
t: T,
}
impl DoAThing for Foo {
fn whatever(self) {
println!("from Foo!");
self.t.whatever();
}
}
takes_a_thing_doer(Foo { t });
}
but this doesn't actually compile, and instead I have to make Foo generic over T (but I cannot use T because it would collide with the outer generic parameter) and then repeate the trait bounds which gets repetitive.
(I would also have liked the following to work, and create a static for each instance of the function, but oh well)
That would be a breaking change. Statics can't have generics and even if they could, silently introducing a generic parameter would be a breaking change as it would cause two instantiations of the outer function to no longer refer to the exact same static, thus changing behavior for interior mutability and for address dependent things. For functions it would cause binary bloat and for type definitions it would cause errors about generic parameters not being referenced anywhere within the type definition.
You could look at that as both a feature and a as a bug, depends on the perspective. As you can see from the comment above by Mokuz, some users might actually want that specific behavior. If you do not want duplicated statics, you don't use generics in them or have a sensible rule and a lint warning the user about potential unintended consequences.
I'm personally less interested in statics though and more in constants. For both CPU and GPU code (with rust-gpu) I use generic constants heavily to occupy exactly as much stack space as is necessary and to help compiler with optimizations by using constants in loops and various expressions (like multiplication/division by a number that is a power of 2), so I can obtain a version of the function optimized specifically for the use case I have.
It is not like the kind of thing I'm asking about isn't possible, it is just not ergonomic and error-prone today. Lifting these limitations in more situations would be a substantial improvement for my use cases.
I don't follow your logic. Yes, code can currently rely on there being only one copy of a static defined inside a generic function -- but that static cannot have any dependency on the function's generic parameters, because that is currently not allowed.
Therefore, if we changed the rule to say that statics may have generic parameters and may also refer to any relevant generic that is lexically available to them, but this causes there to be one copy of that static for each instantiation -- the same monomorphization process as is used for functions -- no existing code would be broken, because there is no existing code that depends on there being only one copy of a static whose definition depends on generics, because right now there aren't any such statics.
To give a concrete example (riffing on @nazar-pc's earlier example):
it would be a breaking change to make Y not be unique across instantiations of expose_three_addresses, but it would not be a breaking change to make X or Z not be unique, because right now X and Z aren't allowed at all.
Having the set of generic params depend on if the body of an item mentions generic params from the parent item sounds rather subtle. Closure upvars introduces cycles that we can only resolve by doing things like typeck and borrowck for both the parent item and all contained closures together. I guess for generic params it would be much less worse as it can be resolved already during name resolution, but still it feels rather subtle.
I recognize it might be a pain to implement, but it's a logical consequence of lexical scoping and therefore I think we should bite the bullet and do it anyway.