The documentation doesn't mention anything about this. The mutation (append/insert) is the last thing I want to do before returning a value from withUnsafeBufferPointer
— I don't plan to touch the buffer at all after — but I'm not sure it's safe to do.
Mutating an array may cause it to allocate new storage even if its count doesn't change. The exact effects in this specific scenario are probably not going to ruin your day, but if you used withUnsafeMutableBufferPointer()
instead then you're going to have a bad time.
I would strongly recommend restructuring your code to perform changes like that outside the call to withUnsafeMutableBufferPointer()
. Here's a (contrived) example of returning a value from within a call to withUnsafeMutableBufferPointer()
:
extension Array<Int> {
/// Increment all elements in this array, then append a new element.
///
/// - Returns: The _former_ last element after incrementing it.
mutating func incrementAll(thenAppend number: Int) -> Int? {
var result: Int?
let count = self.count
self.withUnsafeMutableBufferPointer { buffer in
for i in 0 ..< count {
buffer[i] += 1
}
if count > 0 {
result = buffer[count - 1]
}
}
self.append(number)
return result
}
}
(Mind you, if you actually needed incrementAll(thenAppend:)
, I would write it very differently!)
No, you cannot mutate the buffer from within withUnsafeBufferPointer
. If you try to do so, you will get a compilation error:
var array: [Int] = []
func foo() {
array.withUnsafeBufferPointer { bufferPointer in
if !bufferPointer.isEmpty {
bufferPointer[0] = 42 // Error: Cannot assign through subscript: subscript is get-only
}
}
}
You can use withUnsafeMutableBufferPointer
to mutate the buffer:
func bar() {
array.withUnsafeMutableBufferPointer { bufferPointer in
if !bufferPointer.isEmpty {
bufferPointer[0] = 42
}
}
}
Note, even here, you can only mutate items within the buffer, but not insert or delete items.
I now realize that you may have been talking about mutating the original array (not the values in the buffer). If that is the case, doing this from within withUnsafeMutableBufferPointer
results in “exclusive access” errors. When doing this from within withUnsafeBufferPointer
, it does not generate this exclusive access error. I will defer to others regarding whether it is technically safe to mutate the original array from its withUnsafeBufferPointer
closure or not.
This is safe, but it will force an unnecessary copy of the array:
func f() {
var x = [1,2,3]
x.withUnsafeBufferPointer { ptr in x.append(5) }
}
Semantically, withUnsafeBufferPointer
receives a borrowed copy of x
, and then passes the address of the array buffer, which is shared by the two copies at this point, to the closure. The read access of x
begins and ends before the closure runs, so inside the closure, we can mutate x
without restriction. But the reference count will be greater than one, because we made this copy of x
first. So the call to append()
must copy the buffer. We cannot modify the original buffer in place, nor can we deallocate it, until the closure returns.
Thanks for the answer! So it's actually this kind of mutation that I was curious about: using Array
's own mutation methods from inside of one of its withUnsafe...
methods. I assumed Array
passed its own buffer directly, but from a safety perspective it sort of makes sense it does this.
Right. Ordinary parameters are guaranteed to be bound to values which will not change[1] during the call. The compiler must copy values to satisfy this if it has no other way to make this guarantee.
At an implementation level, an Array
value is a reference to a buffer that stores the elements, so a "copy" just means increasing the reference count on that buffer. Array
uses a discipline in which the elements in the buffer can only be mutated when the reference is unique, so this has the desired effect under value semantics of preventing the elements from changing during the call as well.
Note that, if Swift did not copy the value here, it would have to borrow it out of x
. That borrow would be a read access to x
that would overlap the entire call to withUnsafeBufferPointer
, which means the mutation to x
would conflict with it under the law of exclusivity. Either choice is memory-safe; copying allows code like this to work (perhaps confusingly, and at a runtime cost), while borrowing forces it to be invalid, but in neither case would we emit code that allows the basic rule to be broken that the value of an ordinary parameter cannot change during a call.
At a basic value level. You can, of course, pass a reference to a class instance whose properties will change during the call. But the reference itself will not change and will remain valid throughout the call. ↩︎
It's also worth noting that from a performance standpoint, this kind of accidental copying cannot happen with Span
. When you project a span
from an array, you begin a read access that won't end until the span's lifetime ends, and the compiler will reject any attempt to mutate the array value at the same location in the meantime. The stronger invariants around spans are not just a memory safety win but a potential performance win as well.