What on earth is "inout"?

inout may be the most baffling API in the Swift language. At first sight, it seems pretty straightforward.

var number = 0
func addOne(_ value: inout Int) {
    value += 1
}
addOne(&number)
print(number)    // 1

Anyone who has written some C-like language would think that it is passing the reference of a number to the function to perform any action within it. Although that would overlap with the behaviour of any reference type and breaks the image of a nice, local logic of value types, it is straightforward enough that people can bear with the idea that inout turns a value type variable into a reference type variable temporarily.

But is it really that way? Let’s first check the official documentation. The definition of inout according to the documentation is:

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.

This behavior is known as copy-in copy-out or call by value result. For example, when a computed property or a property with observers is passed as an in-out parameter, its getter is called as part of the function call and its setter is called as part of the function return.

So, according to this copy-in copy-out behaviour, it does not pass the reference to the number variable but copies the value as the parameter and writes the function output back to the number variable.

Therefore, the code above would be quite similar to writing the following:

var number = 0
func addOne(_ value: Int) -> Int {
    return value + 1
}
number = addOne(number)
print(number)  // 1

If the story ends here, it may simply be strange behaviour as defined by Swift. However, things start to get even stranger when we delve deeper into it.

Let’s add a print statement inside the addOne(_:) function scope that prints the number variable.

var number = 0
func addOne(_ value: Int) -> Int {
    print("before: ", number)
    return value + 1
}
number = addOne(number)
print("after: ", number)

When executed, this will print:

before:  0  
after:  1

This is pretty much what we expect. Let’s instead check the implementation with inout.

var number = 0
func addOne(_ value: inout Int) {
    print("before: ", number)
    value += 1
}
addOne(&number)
print("before: ", number)

We would expect to print the number as 0 since nothing has been added yet. However, let’s see what happens.

Simultaneous accesses to 0x106344130, but modification requires exclusive access.
Previous access (a modification) started at  (0x104a10418).
Current access (a read) started at:
0    libswiftCore.dylib                 0x000000019294e17c swift::runtime::AccessSet::insert(swift::runtime::Access*, void*, void*, swift::ExclusivityFlags) + 428
1    libswiftCore.dylib                 0x000000019294e390 swift_beginAccess + 72
4    InoutExp                           0x00000001047767f8 main + 0
5    CoreFoundation                     0x00000001803ee450 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 20
6    CoreFoundation                     0x00000001803eda4c __CFRunLoopDoBlocks + 352
7    CoreFoundation                     0x00000001803e813c __CFRunLoopRun + 788
8    CoreFoundation                     0x00000001803e7aec CFRunLoopRunSpecific + 572
9    GraphicsServices                   0x000000018e7cdb20 GSEventRunModal + 160
10   UIKitCore                          0x00000001852bac78 -[UIApplication _run] + 868
11   UIKitCore                          0x00000001852bebd8 UIApplicationMain + 124
12   InoutExp                           0x00000001047767f8 main + 348
Fatal access conflict detected.

There is a simultaneous access issue where we try to print the higher-scoped number. If you set a breakpoint after the value is modified, you would see that both the higher-scoped number and the lower-scoped value are updated to the new value.

Okay, this looks pretty much like the behaviour of passing by reference. But it is not in alignment with the documentation here. What happened here?

If we take a closer look at the documentation, there is some additional information about this.

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

So, according to this description, it is, by definition, copy-in copy-out. But under the hood, it is, in fact, passed by reference.

According to the git history, the description of optimization with pass by reference has been around since Swift 2.1.

What about under the hood?

This is just the documentation, so you might wonder what it is like under the hood. Luckily, looking into how Swift code is compiled is rather straightforward, as we can easily dump the SIL generated during the compile process.

We will use this example for our experiment.

var number = 0
func addOne(_ value: inout Int) {
    value += 1
}
addOne(&number)

We would use the following command for this purpose. According to the official documentation, this command will ‘print the SIL immediately after SILGen’:

swiftc -emit-silgen -O file.swift

After we run the command on the file, we see the following code in the terminal window.

sil_stage raw

import Builtin
import Swift
import SwiftShims

@_hasStorage @_hasInitialValue var number: Int { get set }

func addOne(_ value: inout Int)

// number
sil_global hidden @$s4test6numberSivp : $Int

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4test6numberSivp                // id: %2
  %3 = global_addr @$s4test6numberSivp : $*Int    // users: %9, %8
  %4 = integer_literal $Builtin.IntLiteral, 0     // user: %7
  %5 = metatype $@thin Int.Type                   // user: %7
  // function_ref Int.init(_builtinIntegerLiteral:)
  %6 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %7
  %7 = apply %6(%4, %5) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %8
  store %7 to [trivial] %3 : $*Int                // id: %8
  %9 = begin_access [modify] [dynamic] %3 : $*Int // users: %12, %11
  // function_ref addOne(_:)
  %10 = function_ref @$s4test6addOneyySizF : $@convention(thin) (@inout Int) -> () // user: %11
  %11 = apply %10(%9) : $@convention(thin) (@inout Int) -> ()
  end_access %9 : $*Int                           // id: %12
  %13 = integer_literal $Builtin.Int32, 0         // user: %14
  %14 = struct $Int32 (%13 : $Builtin.Int32)      // user: %15
  return %14 : $Int32                             // id: %15
} // end sil function 'main'

// Int.init(_builtinIntegerLiteral:)
sil [transparent] [serialized] @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int

// addOne(_:)
sil hidden [ossa] @$s4test6addOneyySizF : $@convention(thin) (@inout Int) -> () {
// %0 "value"                                     // users: %7, %1
bb0(%0 : $*Int):
  debug_value %0 : $*Int, var, name "value", argno 1, expr op_deref // id: %1
  %2 = metatype $@thin Int.Type                   // user: %9
  %3 = integer_literal $Builtin.IntLiteral, 1     // user: %6
  %4 = metatype $@thin Int.Type                   // user: %6
  // function_ref Int.init(_builtinIntegerLiteral:)
  %5 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %6
  %6 = apply %5(%3, %4) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %9
  %7 = begin_access [modify] [unknown] %0 : $*Int // users: %10, %9
  // function_ref static Int.+= infix(_:_:)
  %8 = function_ref @$sSi2peoiyySiz_SitFZ : $@convention(method) (@inout Int, Int, @thin Int.Type) -> () // user: %9
  %9 = apply %8(%7, %6, %2) : $@convention(method) (@inout Int, Int, @thin Int.Type) -> ()
  end_access %7 : $*Int                           // id: %10
  %11 = tuple ()                                  // user: %12
  return %11 : $()                                // id: %12
} // end sil function '$s4test6addOneyySizF'

// static Int.+= infix(_:_:)
sil [transparent] [serialized] @$sSi2peoiyySiz_SitFZ : $@convention(method) (@inout Int, Int, @thin Int.Type) -> ()



// Mappings from '#fileID' to '#filePath':
//   'test/test.swift' => 'test.swift'

Okay, this is not the easiest thing to read in the world. We can pinpoint some noteworthy parts here.

First, let’s take a look at the main function.

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4test6numberSivp                // id: %2
  %3 = global_addr @$s4test6numberSivp : $*Int    // users: %9, %8
  %4 = integer_literal $Builtin.IntLiteral, 0     // user: %7
  %5 = metatype $@thin Int.Type                   // user: %7
  // function_ref Int.init(_builtinIntegerLiteral:)
  %6 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %7
  %7 = apply %6(%4, %5) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %8
  store %7 to [trivial] %3 : $*Int                // id: %8
  %9 = begin_access [modify] [dynamic] %3 : $*Int // users: %12, %11
  // function_ref addOne(_:)
  %10 = function_ref @$s4test6addOneyySizF : $@convention(thin) (@inout Int) -> () // user: %11
  %11 = apply %10(%9) : $@convention(thin) (@inout Int) -> ()
  end_access %9 : $*Int                           // id: %12
  %13 = integer_literal $Builtin.Int32, 0         // user: %14
  %14 = struct $Int32 (%13 : $Builtin.Int32)      // user: %15
  return %14 : $Int32                             // id: %15
} // end sil function 'main'

We first see a few commands that appear to be a direct correspondence to this code.

var number = 0

We can see at first %3 is pointed to a global address and an integer is initialised to %7, and then %7 is stored to %3.

%3 = global_addr @$s4test6numberSivp : $*Int    // users: %9, %8
%4 = integer_literal $Builtin.IntLiteral, 0     // user: %7
%5 = metatype $@thin Int.Type                   // user: %7
// function_ref Int.init(_builtinIntegerLiteral:)
%6 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %7
%7 = apply %6(%4, %5) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %8
store %7 to [trivial] %3 : $*Int                // id: %8

Then the part we are interested in comes.

%9 = begin_access [modify] [dynamic] %3 : $*Int // users: %12, %11
// function_ref addOne(_:)
%10 = function_ref @$s4test6addOneyySizF : $@convention(thin) (@inout Int) -> () // user: %11
%11 = apply %10(%9) : $@convention(thin) (@inout Int) -> ()
end_access %9 : $*Int                           // id: %12

It uses %9 to begin modifying access to %3, and it passes the entire %9 to the function reference. It then marks the end access to %9 after execution.

So, if we want to investigate further, we need to focus on the addOne(_:) part of the SIL.

// addOne(_:)
sil hidden [ossa] @$s4test6addOneyySizF : $@convention(thin) (@inout Int) -> () {
// %0 "value"                                     // users: %7, %1
bb0(%0 : $*Int):
  debug_value %0 : $*Int, var, name "value", argno 1, expr op_deref // id: %1
  %2 = metatype $@thin Int.Type                   // user: %9
  %3 = integer_literal $Builtin.IntLiteral, 1     // user: %6
  %4 = metatype $@thin Int.Type                   // user: %6
  // function_ref Int.init(_builtinIntegerLiteral:)
  %5 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %6
  %6 = apply %5(%3, %4) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %9
  %7 = begin_access [modify] [unknown] %0 : $*Int // users: %10, %9
  // function_ref static Int.+= infix(_:_:)
  %8 = function_ref @$sSi2peoiyySiz_SitFZ : $@convention(method) (@inout Int, Int, @thin Int.Type) -> () // user: %9
  %9 = apply %8(%7, %6, %2) : $@convention(method) (@inout Int, Int, @thin Int.Type) -> ()
  end_access %7 : $*Int                           // id: %10
  %11 = tuple ()                                  // user: %12
  return %11 : $()                                // id: %12
} // end sil function '$s4test6addOneyySizF'

We first see that it creates the number 1 and stores it in %6 for later execution.

%2 = metatype $@thin Int.Type                   // user: %9
%3 = integer_literal $Builtin.IntLiteral, 1     // user: %6
%4 = metatype $@thin Int.Type                   // user: %6
// function_ref Int.init(_builtinIntegerLiteral:)
%5 = function_ref @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %6
%6 = apply %5(%3, %4) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %9

Then we see that it begins accessing %0, which is the passed value from outside the function, and points to it in %7, and directly applies the execution to %7, and ends access to it.

%7 = begin_access [modify] [unknown] %0 : $*Int // users: %10, %9
// function_ref static Int.+= infix(_:_:)
%8 = function_ref @$sSi2peoiyySiz_SitFZ : $@convention(method) (@inout Int, Int, @thin Int.Type) -> () // user: %9
%9 = apply %8(%7, %6, %2) : $@convention(method) (@inout Int, Int, @thin Int.Type) -> ()
end_access %7 : $*Int

There is no point where any value is copied out of %7 and performs any action, and then written back to %7, according to what we see above.

The begin_access and end_access pair is according to the documentation:

begin_access identifies the address of the formal access and end_access delimits the scope of the access.

According to the description, it appears that it is directly locking and sending the memory address to the function reference — which is probably the reason why we are seeing a ‘Simultaneous accesses to 0x106344130, but modification requires exclusive access’ error in the earlier experiment.

Conclusion

inout is a strange concept. At first glance, it appears to be passing an address to something. Just when you think it breaks the value type model, it reveals that it actually follows a safer model — the copy-in copy-out model. However, just when you begin to understand and make sense of it, it surprises you again by stating that it does not want the overhead of copy-in copy-out, so it shares the same memory address inside and outside the function. Just when you think, ‘so, it’s pass by reference,’ they confirm, but explain it’s just an optimization and you should pretend it’s copy-in copy-out, even though it doesn’t seem to be used anywhere under the hood.

So what is inout exactly? It is defined as copy-in copy-out, but its implementation is pass by reference. Who knows, maybe someday we will see the real copy-in copy-out model make a comeback — or come into effect for real?