What to use in swift instead of const references?

i’ve been writing a lot of C++ recently, and one of differences between C++ codebases (at least the ones i work on) and swift codebases is C++ makes a lot of use of “const pointers” whereas swift tends to insist on either value semantics or mutable reference semantics.

i have mostly been sticking to value semantics for as long as i have been using swift, so i’ve only recently begun dipping my toes into the world of non-COW types. and i dislike how in swift, if a type has shared state that can be mutated somewhere, then that state can be mutated anywhere.

there doesn’t seem to be a good way to, say create a data structure (like an array) of a reference type...

let references:[MyReferenceType]

run some algorithm over the reference handles...

// changes the values stored through the references
self.process(references)

and then pass them to some other code that we simply have to trust will not corrupt the data stored through the references.

// does not change the values stored through the references
self.render(references)

i once thought reference types were smelly and the process(_:) step was better performed using value mutations through a subscript and index. and most of the time this is true. but there are some data structures more sophisticated than an array where access is non-trivial, and many algorithms where being able to take references to things and mutate the values through them is much easier than trying to shoehorn them into _modify accesses.

so the issue them becomes: how do we pass [MyReferenceType] to render(_:) without losing the information about its nonmutating-ness?

  1. we can factor the reference-iness of MyReferenceType into a MyValueType and a MyValueType.Box (a class). but then the box needs to grow accessors to all of MyValueType’s API, and that type can have a lot of API.

  2. we can have MyValueType and MyReferenceType coexist, and map [MyReferenceType] to [MyValueType] before passing it to render(_:). this is essentially a lazier version of #1, except instead of tons of accessor boilerplate, we now have duplicated stored properties and monster memberwise inits.

  3. we can do nothing, and accept that we can no longer reason about the values stored through the references after passing them to render(_:).

perhaps there is no solution, and using non-COW types in swift is like bringing skis to a golf course. but i am always curious to hear if anyone has had success programming with non-COW types in swift.

Would the following work for you in principle?

class MyReferenceType {
    var value = 0
}

var references: UnsafeMutablePointer<MyReferenceType>
references = malloc(MemoryLayout<MyReferenceType>.stride * 10)!.assumingMemoryBound(to: MyReferenceType.self)
for i in 0 ..< 10 {
    references[i] = MyReferenceType(/* ... */)
}
func process(_ references: UnsafeMutablePointer<MyReferenceType>) {
    for i in 0 ..< 10 {
        references[i].value += Int.random(in: 0...10)
    }
}
func render(_ references: UnsafeMutablePointer<MyReferenceType>) {
    for i in 0 ..< 10 {
        print(references[i].value)
    }
}

It's not nice in the current form, but you can wrap it in some cleaner and safer Array-like wrappers.

1 Like

I ain't afraid of pointers and mallocs, but is there a way to allocate references without using malloc here? :slight_smile:

Sure thing,

references = UnsafeMutablePointer<MyReferenceType>.allocate(capacity: 10)

these have the same signature, and both of them allow writes to MyReferenceType.value. i’m also not seeing what UnsafeMutablePointer is contributing here, can you explain?

If the problem is boilerplate, we can do something about that. Anything that is expressible as a keypath can be forwarded using dynamic member lookup:

@dynamicMemberLookup
struct MyValueType {

  final class Storage {
    var someMember: Int

    init() {
      someMember = 42
    }
  }

  var storage = Storage()

  subscript<T>(dynamicMember member: KeyPath<Storage, T>) -> T {
    storage[keyPath: member]
  }
}

var val = MyValueType()
print(val.someMember)  // 42

// val.someMember = 99
// Error: Cannot assign to property: 'val' is immutable

If you have a lot of things which cannot be expressed as keypaths (i.e. functions), you can use a macro to generate the forwarding functions. Of course, you would need to annotate whether or not a function should be considered mutating, as that information is not present in the underlying reference type.

Past discussion (briefly) on why Swift doesn’t have C++-style mutability checking for classes:

You need something like Rust’s unique &mut to make the non-mutating methods more than just hints to the reader. Not that hints to the reader aren’t useful, but they weren’t something we considered sufficiently useful in this case to build into the language, especially when many uses of classes with interesting mutability are better expressed as structs in Swift (and a good chunk of the rest would correspond to patterns in Objective-C, which does not have this distinction either).

2 Likes

I believe this is undefined behavior in Swift. malloc allocates raw memory that is not bound to any type, so you cannot call assumingMemoryBound on the results of malloc. Instead, you should use the bindMemory method to bind the allocated memory to a type and get an UnsafeMutablePointer to it, or just use the UnsafeMutablePointer.allocate method.

BTW, instead of malloc I should have written calloc to get zero initialised memory.

I know it could break in theory, could you provide an example when it breaks in practice?
That's a genuine question, I'd really like to see the difference in asm in godbolt, etc to know this stuff better.

a similar solution — with less boilerplate and zero overhead — could be to create a MyProtocol to which MyReferenceType conforms, where MyProtocol declares get-only properties + nonmutating methods for each field on your type.

protocol MyProtocol {
  var field: Int { get }
}

final class MyReferenceType: MyProtocol {
  var field: Int
}

func process(_ values: [some MyProtocol]) {
  // values[*].field is immutable here
}

let references = [MyReferenceType(field: 0)]
process(references)

This feels like the most Cocoa-esque approach to me because it's similar in spirit to how Foundation handles class clusters and mutability (NSString/NSMutableString etc.)

1 Like

what i actually ended up doing was sort of the opposite of this — i created a struct Storage, and then a class StorageReference that wraps it.

final
class ScalarReference
{
    final private(set)
    var value:Scalar

    init(value:Scalar)
    {
        self.value = value
    }
}

and then i factored the mutation logic into methods on the StorageReference class, which actually turned out to be a natural place for them to live, because it means the Storage struct can become completely passive value data. then when i am done with mutations, i extract the Storage values by mapping to \.value.

i have long wished and pretended this were true, but unfortunately even something as simple as a [String: T] map has non-trivial value access. sometimes we can use Dictionary.Index to perform value access, but this API has awful ergonomics, and cannot survive insertions into the dictionary.

i am interested in hearing if anyone has had success finding alternatives to reference access for [String: T].

I don’t understand what you mean by “value access” and “reference access”. Are you saying you have a shared dictionary that’s sometimes mutated? Or that you have a dictionary containing class instances that you don’t want to accidentally mutate the properties of? Or something else?

the second; the “dictionary” is only locally mutated.

value access means accessing values stored inline in the data structure; writing to them involves mutating through a _modify accessor on the data structure.

reference access means accessing references stored inline in the data structure, which wrap values stored outside the data structure; writing through them involves retrieving the reference from a (non-mutating) get (or _read) on the data structure, and performing mutations through the reference.

in both situations, the data structure itself can be mutated (e.g., inserting or deleting entries), but with reference access, the data structure only serves as a means of resolving references. with value access, the data structure is responsible for both lookup and mutation of the values themselves.

1 Like