[swift-users] inout params seem to have undefined behavior

Loïc Lecrenier loiclecrenier at icloud.com
Sat Jun 11 17:50:50 CDT 2016


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 at 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 at swift.org> wrote:
> 
>> 
>>> On Jun 11, 2016, at 11:57 AM, David Sweeris via swift-users <swift-users at 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 at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-users
> _______________________________________________
> swift-users mailing list
> swift-users at swift.org
> https://lists.swift.org/mailman/listinfo/swift-users

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-users/attachments/20160612/c948e1e1/attachment.html>


More information about the swift-users mailing list