A(n anti)pattern for "deinitializing" structs(?)

Let's say I have some data structure S that allocates some memory to a pointer. It's a struct because I want to avoid double indirection when accessing the data pointed to by its pointer, ie:

struct S {
  var somePtr: UnsafeMutableRawPointer

  init() {
    somePtr = UnsafeMutableRawPointer.allocate(byteCount: 1024, alignment: 16)
    print("Allocated", somePtr)
  }

  func doSomething() { print("Does something") }

  func deallocate() {
    print("Dellocating", somePtr)
    somePtr.deallocate()
  }
}

When using S, I have to remember to call deallocate():

let s = S()
s.doSomething()
s.deallocate()

or:

let s = S()
defer { s.deallocate() }
s.doSomething()

Now, and here's the thing I'm wondering about, AFAICS the deallocation can be done implicitly at the end of an S-instance's lifetime, like so:

struct S {
  var somePtr: UnsafeMutableRawPointer

  private final class Deinitializer {
    var block: () -> Void
    init(_ block: @escaping () -> Void) { self.block = block }
    deinit { block() }
  }
  private let deinitializer: Deinitializer

  init() {
    somePtr = UnsafeMutableRawPointer.allocate(byteCount: 1024, alignment: 16)
    print("Allocated", somePtr)
    deinitializer = Deinitializer { [somePtr] in
      print("Dellocating", somePtr)
      somePtr.deallocate()
    }
  }

  func doSomething() { print("Does something") }
}

func test() {
  let s = S()
  s.doSomething()
}
test()

Is this a reasonable thing to do for types like S?

The only downsides I can see are:

  • It adds 8 bytes of storage to the struct.
  • It has to allocate and deallocate the class instance, and call deallocate via indirection. This is only a problem if S instances are created and destroyed with high frequency, in which case allocating and deallocating somePtr would already be a problem.
  • No explicit control over deallocation, which seems like it's not a big deal, because the same control is still available via do { ... }.

Are there more?

2 Likes

Sure. In fact, it's even used in the standard library.

It doesn't have to call deallocate via indirection. You could replace the closure with a copy of the pointer, like this:

private final class Deinitializer {
    let ptr: UnsafeMutableRawPointer
    init(_ ptr: UnsafeMutableRawPointer) { self.ptr = ptr }
    deinit { ptr.deallocate() }
}

Now what you've created is an "owner" for the memory pointed to by ptr. The fact that you're storing a copy of that pointer value alongside it in a (pointer, owner) pair to avoid double indirection doesn't matter.

A similar pattern is used for shared Strings (which, even though not publicly exposed, are baked in to the ABI). Take a look at implementation of __SharedStringStorage (and the draft implementation to see how it is used). It does a similar thing with (pointer, owner) pairs:

final internal class __SharedStringStorage {
  internal var _owner: AnyObject?
  internal var start: UnsafePointer<UInt8>
  ...
}

Here, as far as the String is concerned, _owner is just "some object I need to retain to keep the memory alive" (and, by implication, something it should release when it no longer needs the memory).

Of course, ManagedBuffer is the preferred way to allocate storage while avoiding double-indirection, but if you have a use-case which can't be modelled with that type, storing a (pointer, owner) pair is a perfectly reasonable implementation strategy IMO.

8 Likes