Ivan Gyulev
Ivan Gyulev

Reputation: 115

Why does rust only allow standalone constant for array size?

I want to use a function that concatenates two arrays with a declaration like so:

fn concatenate<const COUNT1: usize, const COUNT2: usize>(a: [i32;COUNT1], b: [i32;COUNT2]) -> [i32;{COUNT1+COUNT2}];

The problem is the return type. Here is the specific error:

error: generic parameters may not be used in const operations
 --> src\main.rs:4:101
  |
4 | fn concatenate<const COUNT1: usize, const COUNT2: usize>(a: [i32;COUNT1], b: [i32;COUNT2]) -> [i32;{COUNT1+COUNT2}] {
  |                                                                                                     ^^^^^^ cannot perform const operation using `COUNT1`
  |
  = help: const parameters may only be used as standalone arguments, i.e. `COUNT1`

This function seems very easy to monomorphize and I don't understand why the compiler doesn't allow it. The rust book only states(twice) it is not allowed, but doesn't explain why:

Const parameters can be used anywhere a const item can be used, with the exception that when used in a type or array repeat expression, it must be standalone (as described below).

As a further restriction, const parameters may only appear as a standalone argument inside of a type or array repeat expression.

Does anyone know how this pattern is against the rust model, because at least from my point of view it definitely isn't an implementation limitation. Here's the whole function if it will help:

fn concatenate<const COUNT1: usize, const COUNT2: usize>(a: [i32;COUNT1], b: [i32;COUNT2]) -> [i32;{COUNT1+COUNT2}] {
    let mut output = [0i32;{COUNT1+COUNT2}];
    output.copy_from_slice(
        &a.iter().chain(b.iter()).map(|&item| item).collect::<Vec<i32>>()
    );
    output
}

Upvotes: 8

Views: 1961

Answers (2)

Chayim Friedman
Chayim Friedman

Reputation: 71410

@Netwave focused on what to do, I want to explain why.

There are two problems with this: implementation problems and design problems.

The implementation problems are various (lots of) bugs with the compilation of such generics that cause ICEs (Internal Compiler Errors, a crash of the compiler) and I think even miscompilations and soundness issues.

The design problem is more problematic: this expression is not as trivial as it seems. What if it overflows?

In a case a constant expression overflows we display an error. Overflow is just one consideration: there are many reasons that can cause an expression to fail to compile (for example, array too big, or access out of bounds). It is easy to reject them when the constant is not generic; but doing so with a generic constant is much harder. We have two choices:

The first is to be like C++. That is, allow that to compile and error if it is actually gets happen, like C++ does with its templates.

The problem with that is that Rust chose deliberately to not be like C++ in the general case, and went with the approach of trait bounds, that is, require the code to be able to compile as generic, instead of requiring it to compile when monomorphized. And this is for very good reasons: post-monomorphization errors are really bad (like C++), and type-checking this is expensive - cargo check does not bail on post-monomorhpization errors, only cargo build. That can cause cargo build to fail why cargo check succeeds, and that is really bad. We already have some post-monomorphization errors, and indeed this is what happens to them:

trait Trait {
    const MAY_FAIL: u8;
}
struct S<const BASE: u8>;
impl<const BASE: u8> Trait for S<BASE> {
    const MAY_FAIL: u8 = BASE + 1;
}

fn cause_post_mono_error<T: Trait>() {
    _ = T::MAY_FAIL;
}

fn main() {
    // This will pass `cargo check`, but fail `cargo build`!
    cause_post_mono_error::<S<255>>();
}

The second approach, which is what generic_const_exprs uses today, is to require every expression that may fail to be repeated in the signature. We earn two things by doing that:

  1. We can check only the signature, and thus at the call site we know this expression will fail - it doesn't satisfy the requirements.
  2. Adding an expression that can fail to the function body, which is a breaking change, is required to be reflected on the signature - and this is good, because Rust's philosophy is that every breaking change has to be reflected on the signature. This prevents hazards like in C++, where even with an extensive test suite you can never be sure some change didn't break something to some customer.

Problem is, requiring to repeat every expression in the signature is, ah, repetitive. And not obvious. So we are still seeking for ideas (here is one). And we cannot stabilize generic_const_exprs until both the design issues and the implementation issues are settled.

Upvotes: 12

Netwave
Netwave

Reputation: 42766

You need to enable the feature generic_const_exprs, which is only available in nightly for the moment (rust 1.63):

#![feature(generic_const_exprs)]

fn concatenate<const COUNT1: usize, const COUNT2: usize>(a: [i32;COUNT1], b: [i32;COUNT2]) -> [i32;{COUNT1+COUNT2}] {
    let mut output = [0i32;{COUNT1+COUNT2}];
    output.copy_from_slice(
        &a.iter().chain(b.iter()).map(|&item| item).collect::<Vec<i32>>()
    );
    output
}

Playground

Upvotes: 7

Related Questions