Confusion with usage of constant arrays

So I wrote this bit of code

const STUFF: [Option<u32>; 3] = [None; 3];

fn main() {
    STUFF[0] = Some(42);
    STUFF[1] = Some(2);
    STUFF[2] = Some(2);
    println!("{:?}", STUFF);
}

And got the following output:

[None, None, None]

The code compiled without warnings with rustc 1.31.0-nightly.

Now, I neither understand why this even compiles, nor why it does and then goes on to ignore those three assignments. Can anybod clear this up?

2 Likes

This is probably a bug, because it looks like it should produce something like E0070, but that error explicitely says that any index expression is a place expression. I believe this is just some previously discovered funkiness with const arrays, but I couldn’t find the issue I’m thinking of…

Usually this confusion comes about because someone meant to use static rather than const. The const values are available to be inlined by the compiler, and if so, they won’t have permanent locations in memory, so naturally we can expect the assignment to do nothing. In other words, you can look at this code as though it is inlining a brand new copy of the array into each line.

Indeed, looking at the MIR we see this:

    let mut _0: ();                      // return place
    let mut _1: [std::option::Option<u32>; 3];
    let mut _2: usize;
    let mut _3: usize;
    let mut _4: bool;
    let mut _5: [std::option::Option<u32>; 3];
    let mut _6: usize;
    let mut _7: usize;
    let mut _8: bool;
    let mut _9: [std::option::Option<u32>; 3];
    let mut _10: usize;
    let mut _11: usize;
    let mut _12: bool;

EDIT: On second thought, I’m not so sure it should be a bug. One can imagine having a const array which contains some const handle to non-const data which could be modified through assignment, and although that probably requires an overloaded assignment operator (and thus isn’t something Rust allows,) I am not certain that there’s no valid reason to treat a const array index expression as an assignable place expression.

Because this is how const works: it basically puts a new copy every time you use it. Your code is equivalent to this:

fn main() {
    [None::<u32>; 3][0] = Some(42);
    [None::<u32>; 3][1] = Some(2);
    [None::<u32>; 3][2] = Some(2);
    println!("{:?}", [None::<u32>; 3]);
}

which also compiles and does the same thing.

3 Likes

Shouldn’t const things be unassignable?

2 Likes

“Technically” they are:

const STUFF: [Option<u32>; 3] = [None; 3];
fn main() {
    STUFF = [None; 3];
}
Compiling playground v0.0.1 (/playground)
error[E0070]: invalid left-hand side expression
 --> src/main.rs:3:5
  |
3 |     STUFF = [None; 3];
  |     ^^^^^^^^^^^^^^^^^ left-hand of expression not valid

error: aborting due to previous error

I think its a more specific question of whether the contents of a const array should be unassignable after resolving the indexing. Must an index operator applied to a const value always return a reference to something const?

EDIT: On third thought… Is there any reason a mutable borrow of const value should be allowed? This currently compiles without error:

const STUFF: [Option<u32>; 3] = [None; 3];
fn main() {
    let s = &mut STUFF;
}

If so, then the OP’s issue would probably resolve as a side-effect of IndexMut not being allowed.

2 Likes

AIUI, the way const values are "inlined" just allows the same things you can do with any other temporary, like:

    let s = &mut [None::<u32>; 3];
    let v = &mut vec![None::<u32>; 3]; // not const, but same idea
1 Like

This is technically wrong, but really helps me deal with const in Rust: const in Rust is more like a text-substitution-macro than a variable or memory location. static is the global variable thingy in Rust.

3 Likes

This is not limited to const array indexing, it applies to const tuple and struct fields as well:
Playground

Since assignment is not overloadable, the only way this could ever do anything is if the const is Drop, like so:
Playground

It is consistent with how consts behave in general, but I also found it confusing when I first saw it.

3 Likes

Maybe there’s a lint that should be made here?

2 Likes

Even though there’s a reason behind this behavior that’s logical from a technical perspective, it’s most likely not what users expect from a thing named const (if it was named copyable_template then maybe…)

I suspect that warning about &mut access to consts would be helpful in explaining this gotcha and maybe even catch mistakes where programmers forget a “variable” is const and assign data to /dev/null.

2 Likes

Not a warning but a lint, I think (i.e. it sounds like a work for Clippy, not the compiler). In my opinion, warnings are something that should always be dealt with in some way (unless you’re in a hurry), and this could have its (rare) use cases.

What are the use-cases? Are there better ways of achieving the same thing?

let x; x = 1 is already a warning, because the assignment is useless. Assignment to a temp copy of an array seems equally useless.

2 Likes

Presumably for the same reason you sometimes pass &mut None to a function: it wants a cache, but you don’t actually care. In contrast, let x; x = 1 legitimately has no use whatsoever.

Incidentally, Clippy already has a similar lint for declare_interior_mutable_const.

1 Like

Honestly it wouldn’t occur to me that I can pass const to a function as a mutable throw-away value. I’d pass literal or a temporary mut variable.

This is due to rust const being more of an “aliased literal” than a const (that would be a static)

And rust allows to &mut a literal, which actually involves using a throw-right-away variable (temp in the comment) :

const X: i32 = 0;

fn main ()
{
    // this errors:
    // X = 42;

    // but this does not:
    *(&mut X) = 42;
    assert_eq!(X, 0);
}

fn real_main () /* X becomes 0_i32 */
{
    // this errors:
    // 0_i32 = 42;

    // but this does not:
    *(&mut 0_i32) = 42; // { let mut temp = 0_i32; *(&mut temp) = 42; }
    assert_eq!(0_i32, 0); // `temp` is never used again
}

For the array example, the &mut is hidden in the sugar of left hand side index assignment:

fn main ()
{
    test();
    test_fully_unsugared();

}

fn test ()
{
    [1_i32][0_usize] = 4;
}

fn test_fully_unsugared ()
{
    *(
        < [i32] as ::std::ops::IndexMut<usize> >::
        index_mut(
            &mut [1_i32] /* "&mut literal" */ ,
            0_usize,
        ) // this is an ephemeral &mut i32, initialised with 1 and where 4 is written to
    ) = 4;
}
2 Likes