I am starting to learn Rust, and while experimenting, I have found a difference in how ownership is applied to tuples and arrays I do not understand. Basically, the following code shows the difference:
#![allow(unused_variables)]
struct Inner {
in_a: u8,
in_b: u8
}
struct Outer1 {
a: [Inner; 2]
}
struct Outer2 {
a: (Inner, Inner)
}
fn test_ownership(num: &mut u8, inner: &Inner) {
}
fn main() {
let mut out1 = Outer1 {
a: [Inner {in_a: 1, in_b: 2}, Inner {in_a: 3, in_b: 4}]
};
let mut out2 = Outer2 {
a: (Inner {in_a: 1, in_b: 2}, Inner {in_a: 3, in_b: 4})
};
// This fails to compile
test_ownership(&mut out1.a[0].in_a, &out1.a[1]);
// But this works!
test_ownership(&mut out2.a.0.in_a, &out2.a.1);
}
The first invocation of test_ownership()
does not compile, as expected Rust emits an error complaining about taking both a mutable and immutable reference to out1.a[_]
.
error[E0502]: cannot borrow `out1.a[_]` as immutable because it is also borrowed as mutable
--> src/main.rs:27:41
|
27 | test_ownership(&mut out1.a[0].in_a, &out1.a[1]);
| -------------- ------------------- ^^^^^^^^^^ immutable borrow occurs here
| | |
| | mutable borrow occurs here
| mutable borrow later used by call
But the thing I do not understand, is why the second invocation of test_ownership()
does not make the borrow checker go nuts? It seems as if arrays are considered as a whole independently of the indexes being accessed, but tuples allow multiple mutable references to their different indexes.
Conclusion. Above is how tuple is saved in Python and Rust. tuple in Python is immutable and the data struct is simpler than list . Rust optimizes the memory layout when using tuple .
The structure of the tuple needs to stay the same (a string followed by a number), whereas the array can have any combination of the two types specified (this can be extended to as many types as is required).
A tuple is a collection of values of different types. Tuples are constructed using parentheses () , and each tuple itself is a value with type signature (T1, T2, ...) , where T1 , T2 are the types of its members. Functions can use tuples to return multiple values, as tuples can hold any number of values.
// Tuples in Rust are comma-separated values or types enclosed in parentheses. let _ = ("hello", 42, true); // The type of a tuple value is a type tuple with the same number of elements.
(A, B, C) // a three-tuple (a tuple with three elements), whose first element has type A, second type B, and third type C Rust tuples, as in most other languages, are fixed-size lists whose elements can all be of different types.
The most trivial data-structure, after a singular value, is the tuple. (A, B, C) // a three-tuple (a tuple with three elements), whose first element has type A, second type B, and third type C Rust tuples, as in most other languages, are fixed-size lists whose elements can all be of different types.
The Rust language today does not support variadics, besides tuples. Therefore, it is not possible to simply implement a trait for all tuples and as a result the standard traits are only implemented for tuples up to a limited number of elements (today, up to 12 included).
Tuples are like anonymous structs, and accessing an element in a tuple behaves like accessing a struct field.
Structs can be partially borrowed (and also partially moved), so &mut out2.a.0.in_a
borrows only the first field of the tuple.
The same does not apply to indexing. The indexing operator can be overloaded by implementing Index
and IndexMut
, so &mut out1.a[0].in_a
is equivalent to &mut out1.a.index_mut(0).in_a
. While a.0
just accesses a field, a[0]
calls a function! Functions can't partially borrow something, so the indexing operator must borrow the entire array.
That is indeed an interesting case. For the second case it works because compiler understands that different part of the structure is borrowed (there's a section in nomicon for that). For the first case compiler is, unfortunately is not that smart (indexing is generally performed by a runtime calculated value), so you need to destruct it manually:
let [mut x, y] = out1.a;
test_ownership(&mut x.in_a, &y);
The difference between the tuple case and the indexing case is what is being used to perform the indexing. In the tuple case we are using syntax sugar over what is effectively an identifier into a struct. As it is an identifier, which field is accessed must be static. As such there is a simple set of rules the borrow checker can follow to determine lifetimes, which follows the same rules used for struct fields.
In the case of an indexing, this is in stable rust an inherently dynamic operation. The index is not an identifier, but an expression. Since the stable typesystem does not yet have a concept of constant expressions or type level values, the borrow checker always treats the index as if it dynamic, even in trivial cases such as yours which are are clearly static. Since they are dynamic, it can't prove non-equality of the indexes, so the borrow conflicts.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With