What is ~Copyable for?

Sorry, I meant "fixed address while locked" to qualify "lightweight locks" but not "atomics". But atomics are also only fixed in memory while undergoing one or more borrows. If nobody is potentially accessing the atomic, then the owner is free to move it (and, in theory, even apply normal exclusive-mutable operations on it) since it has exclusive ownership.

You can only get a RawSpan from a Span over BitwiseCopyable values. Atomic types are not BitwiseCopyable.

3 Likes

Thanks for your thoughtful answer. If you don't mind I'd like to dig into your points a bit further.

Nit: the question here is not whether CoW makes sense, since that is just an implementation technique for value semantics, but whether copyable value semantics makes sense.

FileHandle” is a bit open-ended semantically, unless you mean a specific one like the one in Foundation—which I will note is not ~Copyable, and I don't think that fact has been a major pain point for anyone.

But we can analyze the question in general:

There are two obvious ways to view a FileHandle as a value:

  1. Its value is just the identity of the open file in the process, like a file descriptor. The file's contents are an incidental value to the FileHandle. In this case, it's like a wire attached to an antenna through which I/O occurs. You can make as many copies of the wire as you want and they all make sense. If the existence of the FileHandle does not prevent other processes from writing the file this is probably the only correct view.
  2. Its value is the contents of the file. This view only makes sense if the existence of the FileHandle prevents other processes from writing the file
    • If the file is only open for reading, there's no point in restricting copies. Each copy is semantically access to a copy of the same data.
    • If the file is open for writing, you could say that the FileHandle's value is really the data stored in the file from which it was created. Mutating the value of a FileHandle with no existing copies could semantically update the file. The copies then could then become semantically disassociated from the file (possibly to be associated later with another file).

The 2nd view above is admittedly not very much like the abstraction we think of as a file handle, but it's worth asking whether file handle is an abstraction we should use. :wink:

Thanks for your checklist.

  • Has mutable state and doesn’t semantically make sense to have multiple owners

Nit: the real question is whether multiple copies make sense. What kinds of values don't make sense to copy? Sean Parent has often told me every value can be copied; it's just a matter of how you define the value. In that view, for example, a copy of a mutex is just a disassociated unlocked mutex, and all mutex values are equal (literally, ==).

Has a meaningful identity such that CoW doesn’t fit well

I've never been able to make a “has a meaningful identity” determination by thinking critically. To me it seems entirely subjective. Can you shed any light on this?

Is intended for performance critical domains where reference counting + uniqueness checks may not be tolerable

Some of the most-cited uses of noncopyable types are for access to OS level I/O resources, but the cost of creating/destroying a class instance is usually in the noise compared to the underlying cost incurred by the OS.

Setting those aside, the cost of reference-counting is not an issue if you never actually copy the thing (and on some processors no worse than a non-atomic increment when non-contended). The cost of uniqueness checks is irrelevant if the thing has only immutable data and very low otherwise (because false negatives are OK you can used more-relaxed atomics). So where is this an actual problem?

Non-Sendability is indeed a smell, but could just be telling us it should be a copyable struct. Some of us would argue we should never use (mutable) classes except as implementation details of types having value semantics :wink:

1 Like

I'm not sure I follow. According to std::unique_ptr - cppreference.com, std::unique_ptr does not provide a copy constructor, so it would have the same behavior as a non-copyable struct in Swift.

3 Likes

The container can define a custom copy constructor in C++, restoring implicit copyability. In Swift you’d have to add an explicit clone() method or something.

4 Likes

Somewhat analogously, though, in Swift you can also wrap your noncopyable payload in a class and then store the object reference in a copyable type, and use copy-on-write to unique the object when necessary.

5 Likes

This isn’t always the case. Recently, I’ve been measuring performance and experimenting with different implementation strategies. While creating class instances is generally fast, it depends on what you're comparing it to. For instance, if you compare it to creating an InlineArray or using a unique, non-copyable owner, the latter is significantly faster.

Additionally, using explicit ownership, even for copyable types, resulted in a 20-40% performance improvement in certain scenarios with generic code.

Creation time of this simple wrappers differs dramatically:

final class ClassBox<T> {
  let value: T
  init(value: T) {
    self.value = value
  }
}

struct NCBox<T>: ~Copyable {
  let value: T
  init(value: T) {
    self.value = value
  }
}

I agree that in day-to-day programming, these overheads usually don’t matter. However, when you start dealing with microsecond-level timings and lower, such overheads can become quite significant.

2 Likes

Those are not cases where the purpose is to manage some underlying OS I/O resource. I'm well aware there can be performance advantages in other cases; I'm specifically addressing the common subcategory of I/O resource management.

1 Like

The answer was focused on where this is commonly useful, as that’s the main question of this topic. Like others, I’ve shared some of my own experiences where using of ~Copyable provided practical benefits.

5 Likes

Also I would not make the assumption that I/O overhead would render refcounting “noise.” If you have a heavily multithreaded application whose threads are all working with the same file handle, I can totally imagine that modeling that file handle as a non-copyable type would provide a significant perf win over modeling it as a class.

3 Likes

Thanks everybody; this was very helpful.

1 Like

Just to add to this, I personally think that the often used example of ~Copyable types to model FileDescriptors or Sockets where the deinit closes the descriptor is a bit misleading. Closing with file descriptors in almost all I/O interfaces is at least throwing, and we should surface this error up to the user even if it isn't all that useful, which isn't possible to model with a deinit. More modern I/O interfaces such as io_uring make closing an asynchronous action, which is also not possible to model with deinit. Having said that, I think ~Copyable for memory resource management as mentioned multiple times here is great, just not for any resource where the destructive action can either throw or is async.

6 Likes

I'm honestly not convinced that Rust was wrong in making Copy a refinement of Clone. Every time I've tried to write an abstraction over a Clonable protocol, I always end up wishing that Copyable types just conformed automatically.

Honestly, these types should have a consuming close() instead of closing in the deinit. Although, in the specific case of file descriptors, there's really no safe way to handle an error on close. The only thing to do is to log it.

4 Likes

For situations like this, but also for transaction-like objects that need an explicit end of lifetime (something like commit() or abort()) it would be very useful to have a language feature where there is no deinit, but the programmer must explicitly call a consuming method.

Those consuming methods then could be async or throws or return a value which a deinit cannot (as easily).

5 Likes

Maybe, but I would be very surprised if that were not just a symptom of the “Swift copies too much” problem, rather than anything inherent to using the class model.

While it's not as prominent a concept as it is in Objective-C, we've never hidden the fact that Swift uses reference counting, and Unmanaged is right there.

It’s not “right there”, you have to go looking for it, and it’s marked unsafe. Important for C interop, not very common at all in pure Swift code.

1 Like

i’d like to add another example of ~Copyable adding constraints that are difficult to understand, even for experienced Swift developers, that i ran into today:

struct Logger: ~Copyable {
    var x: Int { 1 }
}
func f(logger: borrowing Logger) {
//     ~~~~~~
// 'logger' cannot be captured by an escaping closure 
//  since it is a borrowed parameter
    var y: [Int: Int] = [:]
    y[0, default: logger.x] += 1
}

i guess ~Copyable things are not compatible with @autoclosure. but why?

LLM diagnosis, which i did not find illuminating

This code fails to compile because of a conflict between Swift's ownership rules for borrowing parameters and the @autoclosure semantics used in the Dictionary subscript.

The Short Answer

The compiler wraps your expression logger.x into an implicit closure (an @autoclosure) to be passed to the dictionary. The compiler determines that this closure is escaping, meaning it could theoretically outlive the function call.

However, logger is a borrowing parameter, meaning it is only guaranteed to exist strictly for the duration of the function f. Swift forbids capturing a borrowed value in an escaping closure because the closure might try to access logger after it has been destroyed or invalidated.

The Technical Breakdown

Here is exactly what is happening under the hood:

1. The Implicit Closure The signature for the dictionary subscript you are using looks roughly like this:

Swiftsubscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value

The @autoclosure attribute tells Swift to automatically wrap the expression you pass (logger.x) into a closure ({ logger.x }). This allows the dictionary to delay evaluating that code until it is sure the key is missing.

2. The "Escaping" Diagnosis The error message explicitly states: captured by an escaping closure. In the context of the += operator (which triggers the _modify accessor of the dictionary), the Swift compiler treats the default autoclosure as escaping. The compiler cannot prove that this closure will strictly execute within the scope of the borrow, so it defensively marks it as escaping.

3. The Borrowing Violation You defined f(logger: borrowing Logger).

  • Borrowing means: "I want to look at this Logger, but I don't own it, and I cannot keep it."
  • Escaping means: "This closure might be stored and executed later, potentially after the function returns."

If the closure escapes, it might try to read logger after f has finished. Since logger is ~Copyable (non-copyable), managing its lifetime is strict. To prevent a "use-after-free" bug, the compiler blocks this capture entirely.

1 Like

That looks like a bug. The default autoclosure should be non-escaping.

5 Likes

I thought I was done here but I have a new take on the question. Let's reframe it as: do non-copyable types have a value, and if so, what is it?

  • The value of a type is intimately tied to the semantics of equality comparison and copying.
  • I don't know of a case where there's no meaningful way to copy a type that has a notional value, do you?
  • In the case cited of managing raw memory allocations, you can meaningfully define the value to be the bytes of the allocation, so the manager becomes the memory and copying it allocates a new block with the same contents.
  • The question in Swift specifically is colored by these facts:
    • Swift copies both implicitly and more than necessary, which makes avoiding copies both urgent and difficult unless you disallow them in the type system. If all copies were explicit, for example, that would be a non-factor. Also the costs of using reference counted CoW things would be a non-factor except for the minor storage costs for the atomic count.
    • Swift doesn't let you define the semantics of copy so if you are defining a “whole” type with a copyable “part,” you have to accept the cost of copying the part in its defined way when copying the whole.
  • That leaves me thinking the only inherently non-copyable things are handles to external resources with no meaningful value or copy operation (e.g. a disk drive), and the other use cases can be viewed as artifacts of the language design.
  • What about these artifacts? Aside from the raw memory block, covered above, do they have meaningful values?
1 Like

I like this angle, I’m pretty happy to say “most non-Copyable types are inherently unique, always un-Equal to each other; the ones that aren’t are ones that have a meaningful notion of copying, but it’s not one that we can expose, or choose to expose, using the tools the language provides (bitwise copies or ARC-based copy-on-write)”.

A motivating example for me here is file descriptors, for which a copy operation exists, dup, but which still refer to the same kernel object post-copy (which can be checked via kcmp on Linux). These definitely have a value (“a reference to a kernel object”), but they also need cleanup; their copy operation can’t be implemented with bitwise copy (and even in C++ you might hesitate to make it implicit); and using ARC-CoW on a value that already has reference semantics wouldn’t really add much. (I’m sidestepping that with ARC you probably don’t need to copy at all most of the time, but that’s not a contradiction.)

4 Likes