Copyable Types
Swift types can be categorized by how they handle copying, which in turn is decided by the type conformance to two marker protocols:
- Copyable: is the ownership of the value type copied or transferred?
- BitwiseCopyable: can the type be copied with a direct memory operation? (like memcpy)
A new syntax was added to explicitly indicate non-conformance to these protocols: a ~
prefix. Therefore, a type can also conform to ~Copyable
(“noncopiable”) and ~BitwiseCopyable
.
Noncopiable Types
A noncopyable type is a struct
or enum
where ownership can be transferred.
- Using a noncopyable type after ownership has been transferred is a compiler error.
- To mark a type as noncopyable, declare it with
~Copyable
.
struct FileHandle: ~Copyable {
let handle: Int32
consuming func delete() {
}
}
let handle = FileHandle(handle: 0)
let moved = handle // transfers ownership
// print(handle.handle) // error: 'handle' no longer owns the struct
print(moved.handle) // OK
Noncopiable types
- can’t be global variables
- can’t conform to any protocols except
Sendable
- can’t appear as properties of other than noncopyable types
- don’t support generics at this time (thus, optionals and arrays of noncopyable are not allowed)
- can have
deinit
, which run automatically at the end of the unique instance’s lifetime (unless explicitly suppressed withdiscard self
in aconsuming
method)
Other limitations
- Changing
Copyable
in a library will break the ABI. - Sometimes an error appears on the original definition of the type, not on the wrong use of it.
- Actors and classes cannot be declared as noncopyable, but they can contain noncopyable stored properties.
Implementation detail: to implement noncopyable types Apple added Copyable conformance to all Swift types. When you conform to ~Copyable
you are overriding this conformance.
The documentation says:
All existing first-class types (excluding nonescaping closures) implicitly satisfy this constraint.In programming language theory, a first-class type or value generally refers to entities that can be:
- Assigned to variables
- Passed as arguments to functions
- Returned from functions
- Stored in data structures
- Actors
- Basic value types (Int, String, etc)
- Classes
- Enums and structs –unless they have noncopyable stored properties
- Functions (except nonescaping closures)
- Protocols
Copyable
language type so I guess all are.
Function signature
Noncopyable parameters can be
borrowing
: read only accessinout
: mutation allowedconsuming
: caller can’t use it after calling the function- Example
struct FileHandle: ~Copyable {
var handle: Int32
}
// Borrowing (read-only access)
func inspect(file: borrowing FileHandle) {
print("handle: \(file.handle)")
// error: cannot assign to property: 'file' is a 'let' constant
// file.handle = 1
}
// Inout (mutation allowed)
func modify(file: inout FileHandle) {
file.handle = 1
}
// Consuming (takes ownership, invalidates original)
func close(file: consuming FileHandle) {
file.handle = 1
print(file.handle)
switch file.handle {
default: ()
}
print(file.handle)
// caller won’t be able to use the file after this
}
var file = FileHandle(handle: 42)
inspect(file: file) // Can still use file after this
modify(file: &file) // Can still use file after this
close(file: file) // Cannot use file after this
// inspect(file: file) // Error: 'file' used after consume
Methods of noncopyable types:
mutating
methods can change the instance- non mutating methods can be
borrowing
(default): allow read accessconsuming
: invalidate the instance once they complete- Example
struct FileHandle: ~Copyable {
var handle: Int32
// Mutating method - can change the instance
mutating func updateHandle(to newHandle: Int32) {
handle = newHandle
}
// Borrowing method (default) - only reads
func readHandle() -> Int32 {
handle
}
// Consuming method - invalidates the instance
consuming func close() {
print(handle)
discard self // prevents deinit from running
}
deinit {
// won’t run if you call close()
print("deinit")
}
}
var file = FileHandle(handle: 42)
_ = file.readHandle() // Can still use file after this
file.updateHandle(to: 100) // Can still use file after this
file.close() // Cannot use file after this
// file.readHandle() // Error: file was consumed
Pattern Matching
0432 refined the behavior of noncopyable structs and enums presented in 0390. I’m using switch
in examples, but the following applies to pattern matching performed with switch
, if
, while
, for
, and guard
.
Pattern matching a noncopyable can either borrow or consume:
- Temporary values (like a function result) are consumed.
- Variables and stored properties are borrowed, but you may consume them adding
consume
. - Example
Example of switch borrow/consume
Variables and stored properties are borrowed by default.
var handle = FileHandle(handle: 42)
switch handle { // borrows by default
case let file: // borrowing binding
print(file.handle)
}
// can still use handle after this
print(handle.handle)
But you may consume them if you add consume
.
var handle = FileHandle(handle: 42)
switch consume handle { // consuming switch
case let file: // consuming binding
print(file.handle)
} // handle is invalid after this
// print(handle.handle) // error: 'handle' used after consume
Temporary values (like a function result) are consumed by default.
func createHandle() -> FileHandle {
FileHandle(handle: 42)
}
switch createHandle() { // consumes by default
case let file:
print(file.handle)
}
Restrictions when switching over noncopyable types:
- The
switch
cannot consume the value during pattern matching where
clauses cannot consume pattern bindings- The value’s ownership can only be transferred once the final match is chosen
- Example
Example of switch restrictions
func f() {
struct FileHandle: ~Copyable {
let handle: Int32
func isReady() -> Bool { true }
consuming func close() -> Bool { true }
}
// warning: variable 'handle' was never mutated
var handle = FileHandle(handle: 42)
// 'where' clause can borrow, but not consume.
switch consume handle {
case let file where file.isReady():
_ = file.close()
// case let file where file.close(): // not allowed!
// print("closed")
default: ()
}
}
f()
Two things:
- There is a false warning on
handle
: it needs to be avar
in order to compile. - I’m wrapping the example on a function because global noncopyable are not allowed.
Bitwise Copyable
A bitwise-copyable type is one that can be safely copied using direct memory operations (like memcpy
), and requires no special cleanup when destroyed.
Examples of bitwise-copyable types are pointers, ints, floats, structs with bitwise-copyable fields. See the whole list in SE-0426 Appendix: Standard library conformers.
To mark a type as bitwise-copyable, conform it to the empty BitwiseCopyable protocol. The only condition is that all stored properties must be BitwiseCopyable
. Example.
Example of BitwiseCopyable types
struct Foo: BitwiseCopyable { // OK
/* empty */
}
struct Foo: BitwiseCopyable { // OK
let x: Int // Int is BitwiseCopyable
var string: String { "\(x)" } // computed
}
struct Foo: BitwiseCopyable { // error
let x: Int
let string: String // stored and NOT BitwiseCopyable
}
Automatic conformance to BitwiseCopyable
is inferred
- for public enums that are
@frozen
- for non public types when:
- All stored properties are bitwise-copyable
- The type is defined within the current module
- The type is not marked with
~BitwiseCopyable
- Example
// an exported type is automatically BitwiseCopyable
struct LocalPoint {
let x: Int
let y: Int
}
// Explicitly not BitwiseCopyable
struct NonBitwisePoint: ~BitwiseCopyable {
let x: Int
let y: Int
}
// an exported type must be explicitly marked
public struct PublicPoint: BitwiseCopyable {
public let x: Int
public let y: Int
}
Generics types can be conditionally bitwise-copyable. Example
struct Container<T> {
var value: T
}
// container is BitwiseCopyable only when T is
extension Container: BitwiseCopyable where T: BitwiseCopyable {}
let intContainer = Container(value: 42) // BitwiseCopyable
let stringContainer = Container(value: "hi") // Not BitwiseCopyable
Limitations of bitwise-copyable types:
- Cannot be applied to global variables
- Cannot extend the protocol
- Cannot be used with dynamic type casting (
as?
,is
) - Example
Examples of BitwiseCopyable limitations
// error: cannot extend protocol 'BitwiseCopyable'
extension BitwiseCopyable {
func copy() -> Self { self }
}
// error: marker protocol 'BitwiseCopyable' cannot be used in conditional cast
struct Point: BitwiseCopyable {
var x: Int
var y: Int
}
let point = Point(x: 0, y: 0)
let isBitwiseCopyable = point is BitwiseCopyable // error
let asBitwiseCopyable = point as? BitwiseCopyable // error
// Error: conditional conformance to non-marker protocol 'MyProtocol'
// cannot depend on conformance of 'Element' to marker protocol 'BitwiseCopyable'
protocol MyProtocol {}
extension Array: MyProtocol where Element: BitwiseCopyable {}
ABI compatibility of a library will break (you will have to recompile its clients) if you add and then remove BitwiseCopyable
. Same as with ~Copyable
. Example.
Not sure if the type will remain bitwise-copyable? better opt-out to preserve ABI compatibility.
// Opting out an otherwise BitwiseCopyable type
// because you plan to add non BitwiseCopyable properties
struct Coordinate4: ~BitwiseCopyable {
var x, y, z, w: Double
}
References
For more details, see