Does inout affect reference count on a class?

What is the difference between passing an instance of a class into a function with inout as opposed to without inout?

E.g.:

func doSomething(with viewController: UIViewController) {
...
}

vs.

func doSomething(with viewController: inout UIViewController) {
...
}

What is the difference if any here, especially in terms of ARC?

I don't believe that there are any guarantees made in terms of reference count (since the optimizer may remove retain/release pairs that are provably unnecessary), but both forms of doSomething hold a strong reference to viewController. The difference is whether viewController itself can be reassigned:

class C {
  var x: Int
  init(x: Int) { self.x = x }
}

func doSomething(_ c: C) {
  c.x += 1
}

func doSomethingInout(_ c: inout C) {
  c = C(x: c.x + 1)
}

with inout on a reference type, we are allowed to reassign the entire instance to something else. When used, we would observe this behavior:

let c1 = C(x: 0)
let c2 = c1
var c3 = C(x: 0)
var c4 = c3

doSomething(c1) // OK
c1 === c2 // true
doSomethingInout(&c2) // Error!

doSomething(c3) // OK
c3 === c4 // true
doSomethingInout(&c3) // OK
c3 === c4 // false
1 Like

Unfortunately it's relatively difficult to say in isolation, because it can depend on surrounding context, the contents of the functions, and how the compiler can transform things in various ways as it optimizes.

If I had to guess in isolation, neither will immediately do a retain, because the first one will pass viewController at +0, and the second one the ARC optimizer will probably successfully optimize the copy-in-copy-out.

1 Like

Interesting, thanks.

Here's an example playground that shows the inout one had a lower retain count.

import SwiftUI
import Foundation

struct MyView: View {
    var body: some View {
        Text("Hello")
    }
}

var host = UIHostingController(rootView: MyView())

print(host)

func doSomething<T>(with host: UIHostingController<T>) {
    print(host)
    print(CFGetRetainCount(host)) // will print 3
}

func doSomethingElse<T>(with host: inout UIHostingController<T>) {
    print(host)
    print(CFGetRetainCount(host)) // will print 2
}

doSomething(with: host)
doSomethingElse(with: &host)

Passing it to CFGetRetainCount() may actually be causing ARC to insert a retain in that case. Also, playgrounds can't reliably be used for this sort of thing because they run extra code on every line.

The only accurate ways to do this are:

  • lldb breakpoints on the various swift_retain/swift_release functions
  • reading the generated assembly code

Working on stdlib performance I do a lot of reading assembly code >.<

Also remember to compile with optimizations enabled

3 Likes