Assuming Copyable objects, the difference between borrowing and consuming is who needs to “make a copy” (increase reference counts) if they’re going to hang on to the object. The callee must take a reference to borrowed objects it’s going to hang on to; the caller must take a reference to consumed objects it’s going to hang on to. In both cases, in the ideal situation, no refcount operation happens, and in the worst case, there is an unnecessary pair of retain/release calls. You need to know if you are going to keep a reference to the object to make the optimal decision, neither is always better or always worse.
Caller drops ref
Caller keeps ref
Callee drops ref
borrowing consuming
borrowing consuming
Callee keeps ref
borrowing consuming
borrowing consuming
where means using borrowing or consuming results in unnecessary refcount traffic and means it results in the optimal amount of refcount traffic. When both the caller and the callee need to keep (or drop) the reference, then there is no refcount traffic difference between using borrowing or consuming. When only one of them needs to keep a reference, then there is a difference.
For initializers, the default is consuming because objects passed to initializers are usually copied into the created objects. If the created object does copy an argument, keeping it consuming is never slower and will occasionally be faster. Conversely, if it doesn’t copy it, making it borrowing will never be slower and will occasionally be faster. This has ABI consequences, so (for public types) you can’t just change it at will and you need to pick the “usually best” default.
In bitCast, again assuming that ~Copyable types are not involved, consuming never beats borrowing because the object that bitCast returns cannot possibly have taken a reference to the argument, so we only need to consider the first row of the table, where borrowing is always optimal. If ~Copyable types are involved, then there is a semantic difference and you might need consuming as a matter of it working at all.
Since we’re not allowing external conformances to ConvertibleToRawBytes yet, no hands are tied any tighter than previously, since the newly-conforming stdlib types are all frozen. This will indeed have to be a consideration for when we expand things.
ConvertibleFromRawBytes is affected by this consideration as well; it makes more types of changes possible source breaks – not just from the compiler but library authors too. Worth thinking about.
MutableRawSpan.storeBytes and OutputRawSpan.storeBytes need BitwiseCopyable because we can't store anything to raw memory that requires deinitialization. Is there any reason we don't include this constraint in ConvertibleToRawBytes?
@_marker public protocol ConvertibleToRawBytes: BitwiseCopyable {}
We’d like to get to a point where examining the bytes of the data you’re holding can be done without having to necessarily invoke unsafe. (This might not be the right place for it, though.) Copying the bytes of a non-BitwiseCopyable thing is not an unsafe operation; loading them as UInt8 for printing is a legitimate thing to do, for example. I believe that an unsafe operation using that copy would require an additional step that does come with an unsafe marker.
For what it’s worth, I don’t think that CopyableToRawBytes is the right too to examine bytes in general because CopyableToRawBytes types must be explicitly declared as such and cannot have padding, whereas there are always going to be situations where you want to examine the raw bytes of something that doesn’t exactly fit there. I wouldn’t find it outrageous if you had to use unsafe code to get the bytes of something slightly out of that model.
Have we given thoughts about how CopyableFrom/ToRawBytes interacts with access modifiers?
Ok, if storeBytes is an implicit bitwise cast (not a copy), then it has the same level of safety regardless of the type restriction. My only concern is that programmers may expect the operation to copy the stored value before storing its bytes.
I've always assumed that casting a non-BitwiseCopyable value to raw bytes should be an explicit bitCast. With this proposal, that bitCast itself becomes a safe operation. You're then free to safely store those bytes. So there's no need for storeBytes to double as a "safe" bitcast.
Once we remove the restriction from storeBytes, we can't go back. So if we want it to imply a non-copying bitCast, let's make sure that choice is intentional and well-communicated.
The as: parameter of storeBytes is problematic for non-BitwiseCopyable types. storeBytes(of: object, as: AnyObject.self) reads as if the buffer will hold a copy of the reference.
For the purpose of bitcasting, it would be better to omit the as: parameter. But we added that out of concern that an implicit conversion could unexpectedly change the bitwise representation of value. We wanted programmers to explictly state the type of the value that bytes are being read from. Maybe that decision was paranoid, but this seems like an API where paranoia is warranted.
For storeBytes to double as a non-copying bitwise cast, I think it would need another type parameter:
public func storeBytes<T: ConvertibleToRawBytes, U: BitwiseCopyable>(
of value: T, toByteOffset offset: Int = 0, as type: U.Type
{
let bitwiseCopyableValue = bitCast(value, to: U.self)
...
So you'd need to write something like:
storeBytes(of: object, as: Int.self)
Or, for now, be content with a redundant-looking bitCast:
storeBytes(of: bitCast(object, to: Int.self), as: Int.self)
For the straight bitCast case, ConvertibleToRawBytes seems the sufficient constraint, but there is a good argument to make storeBytes require ConvertibleToRawBytes & BitwiseCopyable, including the fact that BitwiseCopyable is the current usage.
As @Douglas_Gregormentioned in the previous pitch, has there been any consideration for subscripting UInt8 as well? An UnsafeRawBufferPointer can do both loads and subscripts and I think it would be good to have both on RawSpan as well.
These subscripts are in the latest version of the proposal, for RawSpan, MutableRawSpan and OutputRawSpan. Ultimately, all three types should conform to the Container protocol with an element type of UInt8.