&[&mut [f32]] can not be used if &[& [f32]] is expected

Why can i not pass a &[&mut f32] when the type &[& f32] is expected as a function argument?

To me it seems they should not differ regarding their memory layout so shouldn't it be easily possible to pass such a slice of slices even i don't want to mutate them.

You can make a function take any kind of reference this way:

fn generic(floats: &[impl Borrow<f32>]) {
    for f in floats {
        let f: f32 = *f.borrow();
    }
}

BTW, &f32 is twice as large as f32 (on 64-bit platforms), much more expensive to access, and [&f32] does not support SIMD acceleration. From performance perspective, it'd be much better to use &mut [f32] if possible, and that type can be easily cast to &[f32]. If the elements are not contiguous, maybe try using an iterator (impl Iterator<Item = &mut f32>), instead of allocating storage for a slice of references?

9 Likes

It is not safe (to say use transmute to achieve this), as with &[&[_]] you can copy out the inner references with their longer lifetime and continue to use them after the outer reference has expired. That could lead to aliasing an active &mut _, which is instant UB.

If you gave both references the outer lifetime, you should be fine. (But I would forego unsafe and use something like the trait approach instead.)

6 Likes

To demonstrate @quinedot's point, here's a program that causes undefined behavior. In release mode, it ends up executing as if 2 is not equal to 2, which is nonsensical.

The only use of unsafe was to convert a &[&mut f32] into a &[&f32]. Since this could lead to undefined behavior, this conversion is not allowed in safe code.

5 Likes

I try to understand what is going wrong in this case.
I looked at the miri output:

   Compiling playground v0.0.1 (/playground)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s
     Running `/playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/playground`
error: Undefined Behavior: trying to retag from <1647> for SharedReadOnly permission at alloc820[0x0], but that tag does not exist in the borrow stack for this location
    --> /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cmp.rs:1930:27
     |
1930 |             PartialEq::ne(*self, *other)
     |                           ^^^^^
     |                           |
     |                           trying to retag from <1647> for SharedReadOnly permission at alloc820[0x0], but that tag does not exist in the borrow stack for this location
     |                           this error occurs as part of retag at alloc820[0x0..0x4]
     |
     = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
     = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <1647> was created by a SharedReadOnly retag at offsets [0x0..0x4]
    --> src/main.rs:21:28
     |
21   |     let duplicate1: &f32 = bad(&array)[0];
     |                            ^^^^^^^^^^^^^^
help: <1647> was later invalidated at offsets [0x0..0x4] by a Unique retag
    --> src/main.rs:22:32
     |
22   |     let duplicate2: &mut f32 = array[0];
     |                                ^^^^^^^^
     = note: BACKTRACE (of the first span):
     = note: inside `std::cmp::impls::<impl std::cmp::PartialEq<&mut f32> for &f32>::ne` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cmp.rs:1930:27: 1930:32
note: inside `main`
    --> src/main.rs:23:8
     |
23   |     if duplicate1 != duplicate2 {
     |        ^^^^^^^^^^^^^^^^^^^^^^^^

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

But I don't understand it.

reading here unsafe-code-guidelines/wip/stacked-borrows.md at master · rust-lang/unsafe-code-guidelines · GitHub

i know the simple answer is UB, one cannot know which optimization the compiler chooses to do if one breaks its rules.
But what does the compiler do here so that x is different between line 11 and 13?

Correct. Once UB occurs, all bets are off. Trying to predict the exact nature of what goes wrong or why is a fool's errand at that point. Anything can happen. "Nasal demons."

I'm going to assume you understand why it's UB (an active &mut _ gets aliased) and are asking why a nonsensical binary was the output in technical or mechanical terms (fool's errand).

With those caveats, one particular interpretation is that it "caches" the value of *x and perhaps *y due to the read and write on the first two lines,[1] doesn't realize it can use the *x cache during the println!s for whatever reason, but does use it for the comparison.

But again, attempting such reasoning is in general terms the wrong mindset.[2] It leads to thinking that UB is defined at the translation or hardware levels.[3] But it is not; UB is defined at the language level. Relying on UB not being exploited due to a particular compiler implementation[4] is extremely shaky ground.[5][6] Being firm about this is one of the key benefits Rust has brought to the industry IMO.

Related: see here for an example of an expression that should logically always be true, but compiles to something that is false due to UB. The article is by Ralf Jung (the author of Stacked Borrows). The blog contains other articles about UB you may find illuminating.


  1. e.g. in a register ↩︎

  2. and just a guess; I can't back the interpretation up whatsoever; I didn't even study the ASM, much less dig into the compiler/LLVM ↩︎

  3. and to attempts to "get away with" UB ↩︎

  4. at a specific version, with a particular set of flags and source code input, and a particular backend (e.g. LLVM) and other parts of the tool set, etc ↩︎

  5. even if you fix most variables like compiler version and flags, how UB may be exploited is an emergent property, not an easily contained set of rules; if you're a library, being included by different downstreams may yield different results for example ↩︎

  6. and even if you get it right, all bets are off again with the next compiler version, any untested set of flags, etc ↩︎

4 Likes

I played a bit around here to have a version that does not make miri complain so i hope it does not contain undefined behavior. I used more unsafe here.

Well... it works the same way in C and C++.

The use of Borrow (as suggested by @kornel) combined with RefCell seems also the right way for keeping things safe if one wants to pass a reference multiple times to a function.

but is this really way to do it?
especially this line *(*(*y).borrow_mut()).borrow_mut() *= 2.0; seems overly complicated to me.

No, that's not idiomatic. Typically one just doesn't use tricks to pass &mut _ multiple times.

Rc<RefCell<T>> is generally used for owned T,[1] not for T = &mut U. So something more idiomatic would be taking Rc<RefCell<f32>>. Assuming you actually needed shared ownership, that is.

Additionally, you don't want to import the Borrow and BorrowMut trait imports along side uses of RefCell . RefCell has inherent borrow and borrow_mut methods that are usually what you want to call, but are for a very different purpose than the Borrow and BorrowMut trait methods by the same name. (RefCell's methods basically return locks on the data, whereas the traits are about being a smart pointer or new type around another type.)

For example, if you had Rc<RefCell<f32>>, and didn't import the Borrow and BorrowMut traits, the code would look like:

fn less_weird(
    x: Rc<RefCell<f32>>,
    y: Rc<RefCell<f32>>,
) {
    *y.borrow_mut() *= 2.0;
    println!("x = {}", x.borrow());
    println!("y = {}", y.borrow());
}

Method resolution finds the RefCell::borrow and borrow_mut methods through the Rc, but only if you haven't imported the traits -- because every type implements Borrow and BorrowMut, so it finds the trait methods for Rc before the RefCells get considered. That's why you don't want to import those traits when using RefCell. (The duplicated method names was poor design on std's part.)


The suggestion to use the Borrow trait was so you could pass either of &[&f32] or &[&mut f32] (or &[f32]), not so you could pass two &mut to the same f32. I.e. it addressed your OP, not the aliasing discussion that followed.


  1. Rc is a shared ownership construct ↩︎

4 Likes