Using `indirect` modifier for `struct` properties

Sure, I'm aware of that—but if the user wants to put all of their stored properties into heap-allocated space, I wonder if they're doing something where they would want finer control over that object and not have it hidden and inaccessible by the language. With enums, they don't have a choice unless they made their payload a class, but that would make the type itself harder to use because it would affect the pattern matching API, so I don't think the analogy quite holds.

However, thinking about it further, I will grant that adding something like @Indirect to the standard library could cause users to easily enter into situations that result in poor performance—someone annotating multiple properties with @Indirect would incur one allocation per property, whereas native language/compiler support could potentially gather all of the indirect properties together into a single combined allocation. So that would be a big advantage to deeper integration.

One critical issue that hasn't been addressed here yet though is how mutation of indirect properties would be handled. This isn't a problem yet for indirect enums or indirect cases because enums can't be modified in place, but indirect struct properties would definitely need to handle copy-on-write correctly for mutation.

Unfortunately, it turns out this is easy to get wrong even in a library-only property-wrapper-based version:

This version doesn't handle copy-on-write correctly, because copying an instance of the struct copies the reference to the class but nothing ever checks it for unique-reference or copies it, so mutating the property in one struct instance causes the other instance to reflect the change. The enum variant doesn't suffer from this problem because it does self-reassignment upon mutation:

Wrapper implementations copied from posts above
@propertyWrapper
enum IndirectE<T> {
  indirect case wrapped(T)

  init(wrappedValue initialValue: T) {
    self = .wrapped(initialValue)
  }

  var wrappedValue: T {
    get { switch self { case .wrapped(let x): return x } }
    set { self = .wrapped(newValue) }
  }
}

@propertyWrapper
class IndirectC<Value> {
  var value: Value

  init(wrappedValue initialValue: Value) {
    value = initialValue
  }

  var wrappedValue: Value {
    get { value }
    set { value = newValue }
  }
}
struct StructWithIndirectE {
  @IndirectE var property: Int = 10
}

struct StructWithIndirectC {
  @IndirectC var property: Int = 10
}

do {
  let original = StructWithIndirectE()
  var copy = original
  copy.property = 20
  print(copy.property)     // 20
  print(original.property) // 10 (👍)
}

do {
  let original = StructWithIndirectC()
  var copy = original
  copy.property = 20
  print(copy.property)     // 20
  print(original.property) // 20 (🔥🔥🔥)
}
9 Likes