Sorry for the terse answer, I’ll try to expand a bit on my reasoning here.
In the Swift book, in “Language Reference” -> “Declarations” -> “In-Out Parameters”, it says:
“You can’t pass the same argument to multiple in-out parameters because the order in which the copies are written back is not well defined”.
Now, I am not 100% sure whether &arr and &arr[2] can be considered the same argument, but I would argue that they can because arr contains arr[2].
And because passing the same argument to two inout parameters is not allowed, the compiler can use an optimization like call-by-reference.
Here is an example, where the compiler assumes the arguments are not the same, and therefore uses call-by-reference instead of copy-in-copy-out:
struct S {
var a: Int
}
func foo(inout s: S, _ a: inout Int) {
a += 1
s.a += 1
}
var s = S(a: 0)
foo(&s, &s.a)
print(s) // prints 2
So, to come back to the original example. Here is what I think is happening.
Even though arrays are value types, internally they use a reference-counted buffer. In order to mutate the array, the internal buffer must be uniquely referenced. If it is not, a new identical buffer is created.
So I’m going to follow the life of these internal buffers in the sample code:
var arr = [1,2,3]
// arr.buffer = buffer1 (new buffer)
// buffer1’s reference count: +1
foo(&arr, b: &arr[2])
func foo(inout a: [Int], inout b: Int) {
// buffer1: +1
let acopy = a
// acopy.buffer = a.buffer (which is buffer1)
// buffer1: +2
a = [4, 5, 6]
// a changes, but it has value semantics and a.buffer’s reference count is > 1
// Therefore a new buffer is created.
// a.buffer = buffer2 (new buffer identical to buffer1)
// Now:
// buffer1: +1
// buffer2: +1
print(acopy) // prints buffer1: "[1, 2, 3]"
b = 99 // b points to address in buffer1 because of call-by-reference optimization
// buffer1[2] = 99
print(a) // prints buffer2: "[4, 5, 6]"
print(acopy) // prints buffer1: "[1, 2, 99]"
// Now a is returned -> buffer2 is returned and stays alive
// acopy not returned -> buffer1’s reference count drops to zero -> it is destroyed
}
print(arr) // prints buffer2: "[4, 5, 6]"
I hope this helps and that I haven’t made any mistake
Loïc
···
On Jun 11, 2016, at 10:52 PM, Loïc Lecrenier via swift-users <swift-users@swift.org> wrote:
Hi,
I think what you said is correct. However, it is not a bug. We can't pass two inout arguments that alias each other because then the behaviour is undefined. It is documented in the Swift book somewhere.
Loïc
Sent from my iPad
On Jun 11, 2016, at 10:36 PM, Jens Alfke via swift-users <swift-users@swift.org> wrote:
On Jun 11, 2016, at 11:57 AM, David Sweeris via swift-users <swift-users@swift.org> wrote:
You can’t pass a `let` as an `inout` argument. I’d guess that’s what’s happening is the `arr[2]` part is creating a temporary var to which the `&` part then provides a reference.
But `arr` is a var, not a let.
`b` is then dutifully modified in the function, but there’s no mechanism for copying it back into `arr` when `foo` returns
No, it gets copied back using subscript assignment. Remember, `inout` isn’t really passing the address of the parameter (although the optimizer may reduce it to that.) It’s literally in-and-out: the caller passes the original value, the function returns the new value, the caller then stores the new value where the old value came from.
I am not a Swift guru, but I think the problem in this example is that there’s a sort of race condition in that last post-return stage: the function has returned new values for both `arr` and arr[2]`, both of which get stored back where they came from, but the ordering is significant because arr[2] will have a different value depending on which of those assignments happens first.
This smells like those C bugs where the result of an expression depends on the order in which subexpressions are evaluated — something like “x = i + (i++)”. The C standard formally declares this as undefined behavior.
The part I’m still confused by is how `acopy` got modified within the `foo` function, since it’s declared as `let`. After staring at this for a while longer, I’m forced to conclude that the compiler decided it could optimize the `b` parameter by actually passing a pointer to the Int and modifying it directly, and that this has the side effect of modifying the Array object that `acopy` is pointing to, even though it’s supposed to be immutable.
In other words, this looks like a compiler bug. I can reproduce it with Swift 2.2 (which is what my `swift` CLI tool says it is, even though I have Xcode 7.3.1 and I thought that was Swift 2.3?)
—Jens
_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users
_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users