Hello, I’m writing a custom String type that operates like Rust’s std::string::String (so ~Copyable).
In Rust, Vec uses a NonNull<T>, which is equivalent to a non-optional non-Buffer pointer in Swift. In the 0 capacity case, rather than null, the pointer is initialised to the alignment of T, which allows an Option<Vec<_>> to be 24 bytes.
My UniqueString type has so far used a UnsafeMutablePointer<UInt8> as its storage, and in init(), it is initialised to .init(bitPattern: 0x8)!. This allows UniqueString? to also be 24 bytes, but I want to start using UnsafeMutableBufferPointer for QoL, and because baseAddress is optional, I would lose that optimisation. What should I do here? I have considered just having a property:
var bufferPointer: UnsafeMutableBufferPointer<UInt8> {
.init(start: _pointer, count: _count)
}
Are you already storing the length of the string in another field? Then yes, the buffer pointer is redundant and you can just store the base pointer; if you later need a buffer pointer, reconstituting one is trivial.
So you're aware, the sizes of Optional<UnsafeMutablePointer<UInt8>> and UnsafeMutablePointer<UInt8> are the same; the .none case is bitwise-equal to the null pointer, so no extra storage is required for it.
struct UniqueString {
var _pointer: UnsafeMutableBufferPointer<UInt8>,
_count: Int,
_capacity: Int
}
But to make copying between different pointers more convenient, I sort of want to fold _capacity into _pointer by using a buffer pointer.
Yes, but unfortunately since UnsafeMutableBufferPointer.baseAddress can be nil, folding _capacity into _pointer would increase the size of UniqueString?.
EDIT: Added missing ? to last sentence - UniqueString → UniqueString?
Sorry, I must be missing something. UnsafeMutableBufferPointer is two words long and contains a base address and a count (which in this context is its capacity). If the base address is nil, the count must be 0. So for the case of an unallocated string, it has zero capacity by definition.
It shouldn't be necessary to "pack" the capacity into the buffer as it's already stored there.
struct MyString: ~Copyable {
/// The buffer containing the string. Its `count` property equals the string's capacity.
private var _buffer: UnsafeMutableBufferPointer<CChar>
/// The number of characters in the buffer that have been initialized.
private var _count: Int
/// The maximum number of characters in the buffer before needing to reallocate it.
private var _capacity: Int {
get {
_buffer.count
}
set {
fatalError("FIXME: implement buffer resizing here as needed")
}
}
init() {
_buffer = .init(baseAddress: nil, count: 0)
_count = 0
}
deinit {
_buffer.deallocate()
}
}
The size of this structure is 24 bytes on 64-bit systems (12 bytes on 32-bit systems).
They're asking about the size of an optional value of their type, not just the type itself.
If they use UnsafeMutablePointer and track capacity separately, then they get this property:
struct MyString: ~Copyable {
private var _buffer: UnsafeMutablePointer<CChar>
private var _count: Int
private var _capacity: Int
init() {
_buffer = UnsafeMutablePointer<CChar>.allocate(capacity: 1)
_count = 0
_capacity = 0
}
}
print(MemoryLayout<MyString>.size) // 24
print(MemoryLayout<MyString?>.size) // also 24!
Based on the generated code, the compiler is recognizing that since the first property is a non-optional pointer, there's no valid all-zeros bit pattern for a MyString and it can use that as the value for nil as MyString? instead.
The compiler can't do the same packing because all-zero is a valid UnsafeMutableBufferPointer.
IMO, this isn't silly at all; it provides convenient access when you need it, but you're optimizing the layout specifically for the nil case to make it fit within the same bit width as a non-nil value. This is just the kind of trick you have to use sometimes when you're doing low-level work.
They're asking about the size of an optional value of their type, not just the type itself.
Exactly, although I don't allocate in the case of the empty string, and initialise the pointer to 0x08 (like Rust's String which uses NonNull::dangling)