Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ownership: differences between tuples and arrays in Rust

Tags:

rust

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.

like image 562
doragasu Avatar asked Jun 20 '20 21:06

doragasu


People also ask

Are tuples mutable in Rust?

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 .

What is the difference between tuple and array in typescript?

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).

What is a tuple in Rust?

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.

What is a tuple in rust?

// 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.

What is a three-tuple rust?

(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.

What is the most trivial data structure in rust?

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.

Is it possible to have multiple traits in rust?

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).


Video Answer


3 Answers

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.

like image 184
Aloso Avatar answered Oct 25 '22 09:10

Aloso


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);
like image 43
Kitsu Avatar answered Oct 25 '22 09:10

Kitsu


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.

like image 1
user1937198 Avatar answered Oct 25 '22 08:10

user1937198