Why Rust can't coerce mutable reference to immutable reference in a type constructor?
Asked Answered
T

3

6

It is possible to coerce &mut T into &T but it doesn't work if the type mismatch happens within a type constructor.

playground

use ndarray::*; // 0.13.0

fn print(a: &ArrayView1<i32>) {
    println!("{:?}", a);
}

pub fn test() {
    let mut x = array![1i32, 2, 3];
    print(&x.view_mut());
}

For the above code I get following error:

  |
9 |     print(&x.view_mut());
  |           ^^^^^^^^^^^^^ types differ in mutability
  |
  = note: expected reference `&ndarray::ArrayBase<ndarray::ViewRepr<&i32>, ndarray::dimension::dim::Dim<[usize; 1]>>`
             found reference `&ndarray::ArrayBase<ndarray::ViewRepr<&mut i32>, ndarray::dimension::dim::Dim<[usize; 1]>>`

It is safe to coerce &mut i32 to &i32 so why it is not applied in this situation? Could you provide some examples on how could it possibly backfire?

Taritariff answered 12/4, 2020 at 15:32 Comment(0)
E
3

Consider this check for an empty string that relies on content staying unchanged for the runtime of the is_empty function (for illustration purposes only, don't use this in production code):

struct Container<T> {
    content: T
}

impl<T> Container<T> {
    fn new(content: T) -> Self
    {
        Self { content }
    }
}

impl<'a> Container<&'a String> {
    fn is_empty(&self, s: &str) -> bool
    {
        let str = format!("{}{}", self.content, s);
        &str == s
    }
}

fn main() {
    let mut foo : String = "foo".to_owned();
    let container : Container<&mut String> = Container::new(&mut foo);

    std::thread::spawn(|| {
        container.content.replace_range(1..2, "");
    });

    println!("an empty str is actually empty: {}", container.is_empty(""))
}

(Playground)

This code does not compile since &mut String does not coerce into &String. If it did, however, it would be possible that the newly created thread changed the content after the format! call but before the equal comparison in the is_empty function, thereby invalidating the assumption that the container's content was immutable, which is required for the empty check.

Elliot answered 12/4, 2020 at 17:7 Comment(1)
I think I got it. If I tried to do the same with the String directly I would get cannot borrow foo as immutable because it is also borrowed as mutable. On the other hand Container is always passed by immutable reference regardless of the content type so this mechanism wouldn't be triggered.Taritariff
K
6

In general, it's not safe to coerce Type<&mut T> into Type<&T>.

For example, consider this wrapper type, which is implemented without any unsafe code and is therefore sound:

#[derive(Copy, Clone)]
struct Wrapper<T>(T);

impl<T: Deref> Deref for Wrapper<T> {
    type Target = T::Target;
    fn deref(&self) -> &T::Target { &self.0 }
}

impl<T: DerefMut> DerefMut for Wrapper<T> {
    fn deref_mut(&mut self) -> &mut T::Target { &mut self.0 }
}

This type has the property that &Wrapper<&T> automatically dereferences to &T, and &mut Wrapper<&mut T> automatically dereferences to &mut T. In addition, Wrapper<T> is copyable if T is.

Assume that there exists a function that can take a &Wrapper<&mut T> and coerce it into a &Wrapper<&T>:

fn downgrade_wrapper_ref<'a, 'b, T: ?Sized>(w: &'a Wrapper<&'b mut T>) -> &'a Wrapper<&'b T> {
    unsafe {
        // the internals of this function is not important
    }
}

By using this function, it is possible to get a mutable and immutable reference to the same value at the same time:

fn main() {
    let mut value: i32 = 0;

    let mut x: Wrapper<&mut i32> = Wrapper(&mut value);

    let x_ref: &Wrapper<&mut i32> = &x;
    let y_ref: &Wrapper<&i32> = downgrade_wrapper_ref(x_ref);
    let y: Wrapper<&i32> = *y_ref;

    let a: &mut i32 = &mut *x;
    let b: &i32 = &*y;

    // these two lines will print the same addresses
    // meaning the references point to the same value!
    println!("a = {:p}", a as &mut i32); // "a = 0x7ffe56ca6ba4"
    println!("b = {:p}", b as &i32);     // "b = 0x7ffe56ca6ba4"
}

Full playground example

This is not allowed in Rust, leads to undefined behavior and means that the function downgrade_wrapper_ref is unsound in this case. There may be other specific cases where you, as the programmer, can guarantee that this won't happen, but it still requires you to implement it specifically for those case, using unsafe code, to ensure that you take the responsibility of making those guarantees.

Kreiker answered 12/4, 2020 at 18:27 Comment(4)
Don't we end up in the same situation without the Wrapper in safe Rust? play.rust-lang.org/…Taritariff
@Taritariff I guess that's a decent counter-example, my code should probably have been more explicit, but if you add type annotations to the println! statements, you'll get a lifetime error in your example but not in mine. Compare my code (updated, compiles) vs your code (updated, does not compile). I'll update my answer to include this.Kreiker
(I guess the problem was that it's not being used as a mutable reference, so it doesn't actually check that it's actually mutable. Adding explicit type annotations would ensure that the mutable reference is being used mutably, which should cause a lifetime error but doesn't.)Kreiker
Yes, that's actually interesting that a definition of a reference variable is not treated as a borrow by the compiler and you must actually use it to cause an error. I guess it gives some more flexibility when organizing the code.Taritariff
E
3

Consider this check for an empty string that relies on content staying unchanged for the runtime of the is_empty function (for illustration purposes only, don't use this in production code):

struct Container<T> {
    content: T
}

impl<T> Container<T> {
    fn new(content: T) -> Self
    {
        Self { content }
    }
}

impl<'a> Container<&'a String> {
    fn is_empty(&self, s: &str) -> bool
    {
        let str = format!("{}{}", self.content, s);
        &str == s
    }
}

fn main() {
    let mut foo : String = "foo".to_owned();
    let container : Container<&mut String> = Container::new(&mut foo);

    std::thread::spawn(|| {
        container.content.replace_range(1..2, "");
    });

    println!("an empty str is actually empty: {}", container.is_empty(""))
}

(Playground)

This code does not compile since &mut String does not coerce into &String. If it did, however, it would be possible that the newly created thread changed the content after the format! call but before the equal comparison in the is_empty function, thereby invalidating the assumption that the container's content was immutable, which is required for the empty check.

Elliot answered 12/4, 2020 at 17:7 Comment(1)
I think I got it. If I tried to do the same with the String directly I would get cannot borrow foo as immutable because it is also borrowed as mutable. On the other hand Container is always passed by immutable reference regardless of the content type so this mechanism wouldn't be triggered.Taritariff
B
0

It seems type coercions don't apply to array elements when array is the function parameter type.

playground

Bedrabble answered 14/11, 2022 at 9:7 Comment(1)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewBufflehead

© 2022 - 2024 — McMap. All rights reserved.