[Focused Re-review] SE-0527: `UniqueArray.reallocate(capacity:)`

SE-0527 has been Accepted in Principle, but we would like to extend a focused review period for one API that is mostly unprecedented in the standard library and packages, and which did not receive much attention on the original thread:

The API in question appears below:

/// Grow or shrink the capacity of a unique array instance without discarding
/// its contents.
///
/// This operation replaces the array's storage buffer with a newly allocated
/// buffer of the specified capacity, moving all existing elements
/// to its new storage. The old storage is then deallocated.
///
/// - Parameter newCapacity: The desired new capacity. newCapacity must be
///    greater than or equal to the current count.
///
/// - Complexity: O(count)
public mutating func reallocate(capacity newCapacity: Int)

In the original review thread, one commenter suggested naming this resize(capacity:) instead of reallocate(capacity:). The proposal authors were willing to take this name, but some members of the LSG feel that it suggests an operation that changes the count rather than capacity and prefer the original name ("it says what it does").

I would also like to pin down the use cases and semantics of this operation a bit more precisely:

  • how necessary is it to have this either grow or shrink vs shrink-only (like e.g. Rust's shrink_to_fit)?
  • if it can grow, should it permit a growth factor to prevent accidentally-quadratic behavior, or reallocate to precisely the given capacity?
  • how much use do people have for shrinking to a specified capacity, instead of simply always shrinking to count?

This focused re-review begins now and runs through June 2, 2026.


Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager via the forum messaging feature. When contacting the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available here.

Thank you,

Steve Canon
Review Manager

2 Likes

Some notes on what some other languages provide for similar operations, in no particular order:

  • In C#, the Capacity property on ArrayList has a setter as well as a getter (and an exception is thrown if the new value is less than count). There is also a trimToSize() method that sets capacity to count.
  • In Java, ArrayList provides trimToSize() that sets capacity to count.
  • In Rust, Vec provides shrink_to_fit() that sets capacity to count. shrink_to takes an argument that is a lower-bound and sets the new capacity to min(capacity, max(count, lowerBound)).
  • In C++, the array-like containers provide shrink_to_fit() that "request" that capacity be set to count.

It's probably worth noting that none of them except C#'s capacity setter appear to permit growing the container (Rust's shrink_to could do so with its API signature, but opts not to; if you want to grow you use reserve or reserve_exact).

5 Likes

I have to say that I’ve written a lot of code that reserves more capacity for expected additions, a very small amount of code that wanted to shrink an allocation to fit, and exactly no code that didn’t fit perfectly into one of those categories.

11 Likes

I can only echo @John_McCall's feedback. I predominantly write code that reserves capacity upfront on arrays, byte buffers, or similar data structures when I know the upper bound of needed capacity. The only place that I have actively shrunk a data structure is when used as an intermediate buffer in, e.g. a networking stack that might grow in size during bursts but needs to be resized when the burst is over to free up memory.

I have also played around with the container protocols in swift-collection, and I have found the prototype DynamicContainer very helpful when writing generic methods that take resizable containers.

So to answer the questions:

I think we need both directions and that growing is more common than shrinking.

So far I found the built-in growth factor behavior of most Swift data structures very good.

For the buffer scenarios where I shrunk the buffer back down I always wanted it to be at least a minimum size because the count often is 0 when shrinking.

4 Likes

Note that UniqueArray also has reserveCapacity, which is the usual spelling for growing a buffer (but cannot shrink). The question here is “should the spelling for shrinking the buffer also support growing it, or would restricting it to shrinking like other languages do let us give it a clearer name?”

1 Like

In my experience a shrink_to_fit() operation is relatively rare but still needed. The cases where I've needed it involve storing thousands of large‑layout structs for long periods, where the exact final number is unknown upfront. The array may be initialised with a capacity, grown dynamically and then shrunk to optimise memory consumption. I find it useful to have a direct way to shrink to count without having to dance with init(minimumCapacity:).
I would prefer to name it shrinkToCount() for clarity.

reallocate(capacity:)is highly needed . The method should reallocate to precisely the given capacity without any automatic growth factor.
When implementing custom data structures on top of Array / OrderedSet / OrderedDictionary I needed the ability to set capacity arbitrarily, not just minimise to count or grow quadrantic. Using reserveCapacity(_:) gives imprecise control because it may round or do nothing. I still have todos in codebase.
reallocate(capacity:) with exact capacity fills a real gap that reserveCapacity do not cover.

3 Likes

How strongly do you feel that these two operations should have distinct spellings? If I'm interpreting correctly you're sketching out the following design:

/// if newValue > capacity {
///   capacity ← max(newValue, growthFactor*capacity)
/// }
mutating func reserveCapacity(_ newValue: Int)

/// capacity ← max(newValue, capacity)
mutating func reserveCapacity(exactly newValue: Int)

/// capacity ← count
mutating func shrinkToCount()

Here reserveCapacity(exactly:) can only grow, and shrinkToCount can only shrink; there is no way to shrink to a size between count and capacity, but we (arguably) gain clarity about what's happening at the call site.

The proposal essentially combines shrinking and the exact operation into one function (strawman name to avoid picking a side):

/// guard newValue >= count else { preconditionFailure() }
/// capacity ← newValue
mutating func setCapacity(_ newValue: Int)

(This could literally be a setter, as in C#, though it's O(n). It's slightly unusual.)

I can also imagine providing a default argument and dropping the precondition so you don't need to pass count in the most common usage (I don't love the naming here, it's just to illustrate the operation):

/// capacity ← max(newValue, count)
mutating func shrinkWrap(minimumCapacity newValue: Int = 0)

This feels like a powerful primitive that gives us everything we need (other than reserveCapacity), but naming it is somewhat tricky.

It depends entirely on what this function actually does. reallocate(capacity:) name explicitly tells the caller that it always replaces the storage buffer with a new one of exactly capacity. reserveCapacity(exactly:) would be a softer variant – it only reallocates if requestedCapacity > currentCapacity and it never shrinks)

reallocate as a term is suitable for both growth and shrink to arbitrary size.
reserveCapacity is suitable for growth.

Personally, I have never needed to shrink to a size between count and capacity. I can only imagine custom collections built on top of a unique array/deque/dictionary requiring this for implementing custom growth strategies. For example, a key‑value container that consumes another instance; if self.isEmpty == true, it could take the buffer of the consumed instance and then reallocate it to a capacity somewhere between count and capacity when the existing capacity is known to be excessive.

I find a precondition trap useful only for debug builds. For release builds the function could safely shrink to count if newCapacity < count. This would give the best of both worlds: debuggable errors during development and graceful fallback in production.

Overall:

  • I see no harm in allowing both growth and shrinkage to arbitrary capacities. The implementation cost is low and it gives users maximum flexibility.
  • The naming between reserveCapacity(exactly:) and reallocate(capacity:) should clearly indicate whether reallocation is guaranteed (reallocate) or conditional (reserve).
  • shrinkCapacityToCount() is a convenience function that is useful on its own with clear semantics and no ambiguity. It would be a welcome addition alongside the more general reallocate (if it will be chosen, and allowing both growth and shrinkage).
2 Likes

As it happens, I like this name more than the alternatives. The C# property you can set is appealing, but that bit too cute and also writing a scalar property causing a linear operation is a bit icky. This echoes what's nice about the property setter syntax, without the associated problems. And I prefer that it allows "capacity" to go into the base name.

That said, I could go either way on the one function to rule them all vs one to grow and one to shrink. The benefit of two functions is the growing one can share the name with the one for Array, helping converge their APIs which is nice given the desire to make UniqueArray drop-innable as a replacement for Array where possible.

2 Likes

To think about what use cases the operations would be used:

  • reserveCapacity(_:) optimizes the case where one has a lower bound for the eventual count.
  • reallocate(capacity:)/setCapacity(_:) optimizes the case where one knows the exact eventual count, or where one has an upper bound for the eventual count that's lower than the current capacity.
  • shrinkToFit()/shrinkToCount() optimizes the case where the current count is an upper bound for the eventual count.

These are all special cases of having a lower bound (possibly the current count) and an upper bound (possibly Int.max). So maybe an operation where one specifies both bounds would be a good generalization:

/// precondition(range.lowerBound >= count)
/// if range.lowerBound > capacity {
///     capacity ← max(range.lowerBound, min(range.upperBound, growthFactor*capacity))
/// } else if range.upperBound < capacity {
///     capacity ← range.upperBound
/// }
mutating func clampCapacity(to range: ClosedRange<Int>)

One use case where such an operation could be useful is where one wants to append to an array, and the eventual count is unknown but has an upper bound. I think the ideal strategy would be to delegate to the standard growth strategy until it would reserve a capacity higher than the upper bound. For example, in Array.filter, the size of the original array is the upper bound, but an unknown number of elements are filtered out.

extension Array {
    func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
        var result = []
        for i in self {
            result.clampCapacity(to: (result.count + 1)...self.count)
            result.append(i)
        }
    }
}
2 Likes

On the one hand, we can treat smaller values as a hint only, and ignore them if it would be expensive to reallocate. On the other hand, this is one of the most frustrating optimization gotchas in other languages where a developer temporarily needs a larger buffer, then is fine with a smaller one, but their stdlib ignores them under the covers. Code should do what it says it does, yeah?

Trust the developer, lest our growth factor or algorithm be a pessimization in some common case we hadn't considered.

I can think of a dozen textbook scenarios where shrinking makes sense ("model the number of people in a restaurant over time, blah blah blah etc. etc. etc.") Whether or not they map to real-world use cases is unclear to me, but I don't think we should prevent shrinking an array due to a lack of imagination.

Again, trust the developer.

Edit: I'll also be satisfied with some other API like shrinkToFit() or reallocateSmaller(capacity:) or whatever, so long as we give the developer a way to shrink these things when that's really what they want to do after all.

2 Likes

Add an optional parameter to specify if shrinking should occur?

public mutating func reallocate (capacity newCapacity: Int, shrinking: Bool = false)