Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Not sure how this inout scenario is safe, because the array gets invalidated upon return

The below code doesn't crash, but I can't reason why, given the "limited" docs available.

func foo(inout a: [Int], inout b: Int) {
    a = []
    b = 99
}

var arr = [1,2,3]

// confusion: where does the "out" of b go to?
// storage for a was already cleared / invalidated
foo(&arr, b: &arr[2])

print(arr)  // arr is empty
like image 543
Karl Pickett Avatar asked Oct 31 '22 15:10

Karl Pickett


1 Answers

I believe this is what is happening. When you assign

a = []

you are pointing a to an new array. The original array still exists in memory and when you do:

b = 99

you are modifying the original array, not the new array that a references.

What evidence do you have that this is the case?

Consider this modification to your experiment:

Case 1:

func foo(inout a: [Int], inout b: Int) {
    a[0] = 4
    a[1] = 5
    a[2] = 6
    b = 99
}

var arr = [1,2,3]

foo(&arr, b: &arr[2])

print(arr)  // prints "[4, 5, 99]" 

Now consider this one:

Case 2:

func foo(inout a: [Int], inout b: Int) {
    a = [4, 5, 6]
    b = 99
}

var arr = [1,2,3]

foo(&arr, b: &arr[2])

print(arr)  // prints "[4, 5, 6]"

Clearly, modifying the individual elements of a is not the same as assigning an array to a.

In Case 1, we modified the original array turning the elements into 4, 5, and 6, and the assignment of b changed a[2] as expected.

In Case 2, we assigned [4, 5, 6] to a which didn't change the original values to 4, 5, and 6 but instead pointed a to a new array. The assignment of b does not change a[2] in this case because a now points to a new array at a different location in memory.

Case 3:

func foo(inout a: [Int], inout b: Int) {
    let a1 = a
    a = [4, 5, 6]
    b = 99
    print(a)   // prints "[4, 5, 6]"
    print(a1)  // prints "[1, 2, 99]"
}

var arr = [1,2,3]

foo(&arr, b: &arr[2])

print(arr)  // prints "[4, 5, 6]"

In Case 3, we are able to assign the original array to a1 before assigning a new array to a. This gives us a name for the original array. When b is assigned, a1[2] gets modified.


From the comments:

Your answer explains why the assignment to b inside the function works. However, when foo ends and copies the inout variables back, at that point I don't see how swift knows to defer deallocating the original array until after the &[2] assignment.

It is likely that this is a result of reference counting by ARC. The original array is passed by reference to foo and the reference count is incremented. The original array isn't deallocated until the reference count is decremented at the end of foo.

It seems just as hairy as what the docs already disallow - passing the same variable twice as inout. Also your case3 is surprising. Shouldn't the let a1 = a line do struct / value semantics and copy a snapshot of the array right then?

Yes. I agree that Case 3 is surprising, but it does reveal some of what is going on under the covers. Normally when you assign one array to a new variable, Swift does not immediately make a copy. Instead, it just points the second array to the first and the reference count incremented. This is done for efficiency sake. It is only necessary to make a copy when one of the arrays is modified. In this case, when a gets modified, a1 keeps the original copy of the array in memory.

That's really confusing; I don't see why a1 wouldn't get 1,2,3. Also a let should be immutable!

The fact that a1 gets modified when b is set shows that a1 is pointing to the memory of the original array. Swift apparently doesn't consider setting b as modifying a1. Perhaps because it already made sure a was mutable when foo was called with &a[2].

like image 81
vacawama Avatar answered Nov 08 '22 08:11

vacawama