[Pitch] Box

@Alejandro, I am unsure if you already answered this question, but is the implementation expected to always heap-allocate space for the value, or is there a future where the compiler and/or runtime can optimize and stack-promote an instance?[1] If it could stack-promote, it could be used to efficiently replace calls to withUnsafeTemporaryAllocation().


  1. I wouldn't rely on the compiler stack-promoting calls to UnsafeMutablePointer.allocate() as it tends to be very conservative about doing so and in practice will refuse to stack-promote allocations that should be safe. ↩︎

1 Like

I’m back again with another set of revisions to the proposal. (as always, the text is still here: Box by Azoy · Pull Request #3067 · swiftlang/swift-evolution · GitHub)

This revision points out is that Unique guarantees a stable address for the value it allocates on the heap. In terms of stable pointer that @ellie20 has been mentioning, we leave that behavior unspecified. We do not believe the Swift project is ready to talk about its aliasing rules with regards to getting the pointer from the box, moving the box, and understanding if that original pointer’s source is still preserved or not (its source aka its provenance). While we do guarantee we will never move the value once allocated, we aren’t exactly proposing an API to get this pointer right now. We leave that to a future proposal once we are ready to talk about the aliasing rules surrounding Unique (in addition to some potential safer alternatives that return Borrow and Inout as being currently proposed here: `Borrow` and `Inout` types for safe, first-class references )

Currently we will always heap allocate. Could there be a future where we can stack allocate it sometimes? Perhaps, but that optimization sort of defeats the entire purpose of Unique which is to force a value on the heap, so most likely not.

So we’ve thought about this quite a bit actually :sweat_smile: We think we would go in the allocator route which could be turned around to achieve the same effect by deallocating memory with free with a custom allocator. This would look something like the following:

public struct Unique<Value: ~Copyable, Alloc: Allocator>: ~Copyable {
  let pointer: UnsafeMutablePointer<Value>
  let allocator: Alloc
}

We would need to propose some Allocator protocol that custom allocators could conform to and provide some SystemAllocator that comes by default in the standard library (similar to SystemRandomNumberGenerator). However, this makes work with this type a little more awkward:

func foo(with x: borrowing Unique<Int>)

error: generic type 'Unique' specialized with too few type parameters (got 1, but expected 2)
1 | protocol Allocator {}
2 | 
3 | struct Unique<Value: ~Copyable, Alloc: Allocator>: ~Copyable {
  |        `- note: generic struct 'Unique' declared here
4 | 
5 | }
6 | 
7 | func foo(with x: borrowing Unique<Int>) {}
  |                            `- error: generic type 'Unique' specialized with too few type parameters (got 1, but expected 2)
8 | 

You could alleviate this with Unique<Int, some Allocator> (or an explicit generic parameter), but you’ve made working with this type much harder than it needs to be. C++ and Rust solve this particular issue with default values for generic parameters. So in our original Unique definition we could have:

public struct Unique<Value: ~Copyable, Alloc: Allocator = SystemAllocator>: ~Copyable

Which helps working with this type significantly. (There was even an attempt to add this functionality here: [WIP] Add default types to generic parameters by Azoy · Pull Request #84452 · swiftlang/swift · GitHub )

All that said, some folks are very hesitant to add default generic parameters for a few reasons:

  1. Forgetting to be generic over the allocator could lead to situations where you provide API for only the system allocator (which isn’t that bad!). ABI stable libraries wouldn’t be able to modify this function definition unless they deprecate the old symbol and add a new one (or used @export(implementation) to begin with).
  2. Default values for generic parameters could lead to a worse developer experience especially when debugging stack traces. C++ is pretty infamous for having ridiculously long specializations that while in source are easy to grok, its output in a stack trace is less so.
  3. Being generic over an Allocator specifically means you now need to care about the copyability of the allocator you’re storing. For SystemAllocator, it would just be a zero sized type that is a wrapper over UnsafeMutablePointer.allocate and deallocate, so we can easily copy it. However, in Rust especially there are many places where API is only available when the allocator is clonable which makes writing the most generic API possible a little more difficult. While the folks on the standard library are happy to deal with these challenges, we can’t guarantee that the community at a whole will. It’s very possible folks extending Unique (or potential future collections that can hold noncopyable values etc.) won’t deal with it and just provide the API where Alloc = SystemAllocator which like I mentioned earlier, is a great default.

Personally speaking, I would love allocator arguments, but there are legitimate concerns for why the Swift project shouldn’t go in that direction.

5 Likes

Not to belabour the point, but how then is this type better than final class Box<T> which could also provide a stable address on the heap while at the same time being Copyable if needed? Under what conditions would I prefer to use a move-only value type that heap-allocates under the covers?

(If it's the cost of an isa then please keep in mind that heap allocation sizes get rounded up, but also is that a sufficient trade-off for the ergonomic costs of a ~Copyable type?)

The Synchronization module has _Cell in it to provide a stable address for mutexes and atomics. Is this type better than that one? If so, under what conditions would _Cell be the better choice (remove the underscore for the sake of discussion), and if there aren't any, should Mutex and Atomic adopt Unique?

2 Likes

The biggest difference is that Unique doesn't have any ARC overhead to optimize out. While most of the time it doesn't matter, there are some use cases where the overhead of reference counting is too much. There are also some use cases where it's important to have exact control over when the physical copy happens, which copy-on-write deliberately doesn't offer, but Unique does.

As for _Cell, I don't think it supports the same use cases as Unique. IIUC, _Cell exists to provide a stable address for inline storage, so that types like Atomic<Int> can store their values directly inside themselves.

1 Like

I’m a bit skeptical of the direction around allocators.

In practice, allocator parameters on containers were rarely handled correctly in C++ library code. Many libraries simply ignored them and relied on the default allocator, which caused friction for users who actually used those type with custom allocators. Others attempted to propagate allocator parameters “properly,” but this tended to force allocator types into public APIs, or led to rapidly spreading templates through the codebase and making otherwise stable abstractions difficult to use or evolve.

These issues (among many others) eventually led to the introduction of std::pmr::polymorphic_allocator. That addition was effectively an admission that allocator-as-template-parameter does not scale well for vocabulary types and modular systems, and that allocator choice often needs to be a runtime concern rather than part of the static type. By the time PMR arrived, however, the model was already complex and confusing for many users.

If allocator handling is going to be part of Unique, I wonder whether a self-referential allocator protocol could offer a more future-proof default, e.g. something like
typealias Unique = _Unique<T, any Allocator>, even if that makes values of Unique type slightly fatter. This would at least avoid baking allocator choice into the generic type by default.

More broadly, Swift does not yet have a first-class notion of allocators. As long as core types like Array can exist without exposing allocator parameters, it is reasonable for Unique to do the same. If Swift later introduces allocators as a concept, Unique could gain allocator support alongside Array and other existing library types, rather than committing to a particular allocator model prematurely.

6 Likes