Very positive on the inclusion of this in the standard library. I've been using UniqueArray in a form similar to the swift-collections version in several projects, and it's great from a performance / usability standpoint.
I have three nontrivial concerns with the proposal.
Containers module isn't needed
I feel that noncopyable containers belong in the standard library alongside their corresponding copy-on-write collections. Copy-on-write is still the right default for most code, but the Unique variants should be right there, in the same core module, for folks who need them. Sequestering them in a separate Containers module makes them less discoverable and less likely to be used when folks should use them.
RigidArray isn't needed
Despite using UniqueArray in a whole bunch of places, I have yet to need RigidArray for anything. In my experience, I've needed either a truly fixed-size array or I've needed a resizable UniqueArray. A RigidArray that traps on an append that exceeds the capacity feels, to me, like more of a footgun than a data structure I would reach for. I think it's fine if it stays in swift-collections for those who need it, but it feels like putting it in the standard library (even in a standard Containers module) is going to lure folks into using it where they aren't thinking about preconditions.
I do wonder if the use cases that motivated RigidArray would be better served by a differently-named append variant that traps rather than reallocations. appendWithoutReallocation is very long, but it makes clear the extra precondition beyond what one would expect of just append.
Sharing a representation with Array
This is covered the alternatives considered, but I want to bring it up for more discussion.
The case for having UniqueArray and Array share their storage representation is that it allows O(1) conversion between the two. For example, a low-level API might produce a UniqueArray because that's the lowest-overhead solution. But a high-level library or user code consuming that UniqueArray wants to traffic in Array because it's easier to work with and commonly used throughout the rest of the program. If there is no shared representation, that requires copying the contents---an O(n) operation with a new heap allocation.
If there's a shared representation between the two, then you can have an Array initializer that consumes a UniqueArray and operates in O(1) time with no memory allocation, e.g.,
extension Array {
init(consuming: consuming UniqueArray<Element>) { /* steal the guts of UniqueArray */ }
}
The other direction is possible when the array is uniquely referenced, so the API might look like this (and is also O(1)):
extension Array {
mutating func consumeIfUnique() -> UniqueArray<Element>? {
if isUniquelyReferenced { return /* steal the guts, then set self = [] */ }
else { return nil }
}
}
Now, the Array representation is larger than what UniqueArray would normally need: it has two pointers worth of storage in it, one for the type metadata pointer (for the class that backs the Array) and one for the reference counts. Those would be untouched or set to constant values by UniqueArray, and filled in at the point where Array consumes the UniqueArray.
That's constant per-instance overhead of two extra pointers for UniqueArray to support this O(1) operation. I think that this kind of layering of libraries is going to become a lot more common in Swift, as lower-level libraries use more noncopyable types for performance/code size reasons while most code keeps on using the copy-on-write types for convenience.
Doug