Accessing a `var` passed as `inout` is undefined behavior?

240 Views Asked by At

From the documentation for inout parameters:

In-out parameters are passed as follows:

  1. When the function is called, the value of the argument is copied.
  2. In the body of the function, the copy is modified.
  3. When the function returns, the copy’s value is assigned to the original argument.

From just this description, I would assume that modification of a variable passed as an inout parameter, within the scope in which it is passed as inout, is meaningless, since the original variable is guaranteed to be overwritten after the call returns. As a contrived example:

var x: Int = 5

({ (inoutX: inout Int) in
    inoutX = 7
    x = 6
})(&x)

print(x) // Expecting "7"

The original variable x is accessible through mutating capture, so it can still be assigned to. The expected printout is "7", since that is the value of inoutX at the end of the function call. But if I run this in the Swift 4 REPL, I actually get "6"!

The documentation sheds some light on this behavior:

As an optimization, when the argument is a value stored at a physical address in memory, the same memory location is used both inside and outside the function body.

But then follows with a statement which is very clearly inaccurate:

The optimized behavior is known as call by reference; it satisfies all of the requirements of the copy-in copy-out model while removing the overhead of copying.

Clearly, the optimized behavior is not satisfying the call-by-value-result convention which inout parameters claim to conform to. The documentation then acknowledges this, but in the reverse, explaining why you should not rely on call-by-reference behavior:

Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.

Do not access the value that was passed as an in-out argument, even if the original argument is available in the current scope. When the function returns, your changes to the original are overwritten with the value of the copy. Do not depend on the implementation of the call-by-reference optimization to try to keep the changes from being overwritten.

What I can gather then, is that inout parameters are call-by-value-result, except when they're call-by-reference. And, based on how much the documentation doesn't want you to rely on call-by-reference semantics, I can only guess that the optimization is not performed in a well-defined set of cases. And if that's the case, then I can only conclude that accessing a variable passed as inout parameter, within the scope in which it is inout, is undefined behavior.

That's a relatively unfortunate conclusion, and I'm left confused by how unwilling the documentation is to make it. Why would it try to present inout parameters as obeying a particular calling convention, when (aside from setters or property observers) the semantics of these calling conventions are not observable in a defined manner? It's confusing enough that I doubt my own conclusion here, and so comes the question: is my understanding correct?

0

There are 0 best solutions below