Swift Noncopyable Type and Variable Ownerships

In Swift 5.9, the Swift team introduced multiple amazing features, such as Macros, variable consuming and Actor Custom Executor.

The Macros is a marvelous enhancement to Swift and for all Swift developers. I will post another article for exploring what the macro is and how to build a macro of our own.

In this post, we will learn a new type Noncopyable and some attributes collaborating with it.

Noncopyable

The value types like struct and enum are implicitly copy-on-write types in Swift. If we assign a exited struct instance to another variable and change the instance’s value, the struct value will duplicate itself to two instances with distinct memory addresses.
Namely the struct and enum values are implicitly copyable and don’t share their inner data in different places.

The Noncopyable type is not a real Swift protocol that we can use it as type declarations. We can only use it decorate struct or enum declarations with a specific attribute ~Copyable.

1
2
3
4
5
6
7
8
9
10
11
struct SecretNumber: ~Copyable {
var value: Int

init(_ value: Int) {
self.value = value
}

deinit {
print("secrete number deinit: \(value)")
}
}

As you can see, a noncopyable struct can declare the deinit method that can only be used in classes usually.
Before diving into, I list some limitations for using noncopyable types:

  • Only structs and enums can be noncopyable.
  • A struct or enum having noncopyable properties must also be noncopyable.
  • Noncopyable types cannot be used in generics situations at this time.

Here is an example for illustrating how the noncopyable type SecretNumber behaves:

1
2
3
4
var number = SecretNumber(666)
let copyNumber = number
print(copyNumber.value) // 666
print(number.value) // compilation error

In above code, the expression let copyNumber = number will assign the number’s data to the new variable copyNumber and the old variable number will be consumed that means the variable number is invalid or uninitialized after assigning.

As a result, when we invoke the expression print(number.value) to access the variable number will trigger a compilation error: 'message' used after consume. We cannot reuse the variable number after it was consumed.

If you watch the output in the console carefully, you will notice that the deinit log was printed before the value printing of the number. This is a slight distinction between noncopyable types and classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ClassNumber {
var value: Int

init(_ value: Int) {
self.value = value
}

deinit {
print("class number deinit: \(value)")
}
}

func main() {
let secretNumber = SecretNumber(100)
print(secretNumber.value)
let classNumber = ClassNumber(200)
print(classNumber.value)
}
// secret number deinit: 100
// 100
// 200
// class number deinit: 200

consuming & borrowing

Besides using noncopyable types as local variables, we need passing them into method as parameters. When we create a normal method that take a parameter of the SecretNumber type, we will get a compilation error: Noncopyable parameter must specify its ownership.
Xcode provides three options for us to resolve this problem:

  • Add borrowing for an immutable reference.
  • Add inout for a mutable reference.
  • Add consuming to take the value from the caller.
    The inout attribute is familiar for us. That means the caller will pass a reference pointer into the inout parameter and any modification to the inout parameter will influence the outer variable.

The borrowing and consuming are two new attributes specially offered to noncopyable types.

1
2
3
func consume(_ number: consuming SecretNumber) {}

func borrow(_ number: borrowing SecretNumber) {}

Literally, the consume method will consume the parameter like assigning the value to another variable. Actually, it is just that.

1
2
3
let number = SecretNumber(100)
consume(number)
print(number.value) // compilation error: `number` used after consume.

Inside the method consume(:), the consuming parameter number still is a normal noncopyable value and we can use it is like using a normal local noncopyable variable.

1
2
3
4
5
func consume(_ number: consuming SecretNumber) {
let copyNumber = number
print(copyNumber.value) // xxx
print(number.value) // compilation error: `number` used after consume.
}

The other attribute borrowing means that the parameter is just borrowing the outer value with readonly mode. You can think it of a const SecretNumber * const number reference pointer.

1
2
3
4
5
6
7
8
9
10
func borrow(_ number: borrowing SecretNumber) {
// borrowing value can be used or borrowed.
print(number.value) // xxx
borrow(number) // ignores the cycle invoking.

// borrowing value cannot be consumed.
// compilation error: `value` has guaranteed ownership but was assumed.
consume(number)
let copyNumber = number // compilation error
}

Otherwise, a noncopyable variable “borrowed” by other methods can still be used in subsequent code.

1
2
3
4
5
6
7
8
9
let number = SecretNumber(999)
borrow(number)
borrow(number)
// The number variable can still be used or consumed after borrowed.
print(number.value) // 999
consume(number)
// or
// let copyNumber = number
// print(number.value) // compilation error

We can also use the consuming or borrowing attribute to decorate functions in noncopyable types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct SecretNumber: ~Copyable {
var value: Int

init(_ value: Int) {
self.value = value
}

deinit {
print("secrete number deinit: \(value)")
}

consuming func consume() {}
borrowing func borrow() {}
mutating func mutate() {}
}

A noncopyable instance invoke a consuming instance method will consume itself and then the instance variable cannot be accessed in subsequent code.

1
2
3
4
let number = SecretNumber(666)
number.consume()

print(number.value) // compilation error.

Inside the consuming function, we can access or consume self.

1
2
3
4
consuming func consume() {
let otherSelf = self // consumes self.
print(value) // compilation error.
}

Inside the borrowing function, we can only read properties but cannot assign self to another variable.

1
2
3
4
5
borrowing func borrow() {
print(value) // xxx

let otherSelf = self // compilation error: 'self' has guaranteed ownership but was consumed.
}

Invoking borrowing instance functions of a noncopyable variable doesn’t affect the variable at all.

1
2
3
4
5
6
7
8
let number = SecretNumber(200)
number.borrow()

print(number.value) // 200
let copyNumber = number // consumes the number.
// or
// number.consume()
print(number.value) // compilation error.

Notes that normal functions without any attributes behave identically as borrowing functions.

We all know functions declared with the attribute mutating can change properties or assign a new value to the self. Can we consume self in a mutating function? The answer is YES. Extra, we need reassign a new value to self after consuming self, because a mutating method is still a normal function that doesn’t consume the variable to outer.

1
2
3
4
mutating func mutate() {
let copy = self
// compilation error: 'self' consumed but not reinitialized before end of function
}

Reminder to reinitialize self in mutating functions of noncopyable types after resuming self.

1
2
3
4
mutating func mutate() {
let copy = self
self = SecretNumber(333)
}

In some special situations, we may do some cleanup work or releasing resources in the deinit function. If we have a consuming function that will do the same operations above, the resources will be released twice separately in the consuming function and the deinit function.
Let’s say we have a FileDescriptor type like below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct FileDescriptor: ~Copyable {
private var fd: CInt

init(descriptor: CInt) { self.fd = descriptor }

func write(buffer: [UInt8]) throws {
let written = buffer.withUnsafeBufferPointer {
Darwin.write(fd, $0.baseAddress, $0.count)
}
// ...
}

consuming func close() {
Darwin.close(fd)
print("close: close file")
}

deinit {
Darwin.close(fd)
print("deinit: close file")
}
}


let file = FileDescriptor(100)
file.write([0, 1, 2])
file.close()

// close: close file
// deinit: close file

Swift has provided a new operator discard that can be used in consuming methods to suppress the invoking of the deinit method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct FileDescriptor: ~Copyable {
...
consuming func close() {
Darwin.close(fd)
print("closed the file.")

discard self
}
...
}

let file = FileDescriptor(100)
file.write([0, 1, 2])
file.close()

// close: close file

We can only use the discard operator in consuming methods with the format discard self and all stored properties of the noncopyable type must be of trivially-destroyed types. If we redefine the number property of our SecretNumber struct to be of String type and discard self in the consume method, we will receive a compiler error.

1
2
3
4
5
6
7
8
9
struct SecretNumber: ~Copyable {
var value: String

consuming func consume() {
discard self // compilation error: Can only 'discard' type 'SecretNumber' if it contains trivially-destroyed stored properties at this time
}

deinit {}
}

There are many behavioral distinctions between trivial or non-trivial types when we use them in the Swift variable ownership system. We will discuss the trivial types and influences produced by them in detail at the end of this post.

consume

The consuming and borrowing attributes are only available with noncopyable types. If we want to consume a normal variable of any type, we can use the new operator consume. A local variable following a consume operator when be assigned to another variable or passed to functions will forward the variable itself, without copying.

1
2
3
4
5
let message = "secret content"
print(message) // Prints content of the message and then destroys it.
_ = consume message

print(message) // compilation error: `message` used after being consumed.

If the local variable is mutable, we can reuse the variable message by reassigning a new value to it after consuming it.

1
2
3
4
5
var message = "secret content"
_ = consume message // Consumes the local variable.

message = "plain text" // Reactivates the local variable.
print(message) // plain text

trivial types & trivially-destroyed types

In the discussion about the discard operator above, we have said the necessary condition is all stored properties of the noncopyable type must be of trivially-destroyed types. This is due to noncopyable types have deinit methods used to safely release resources, but the discard self syntax will suppress the invocation of the deinit method. That means resources in the object may not have been released correctly and produced unexpected behaviors and memory leaks.

The main feature of trivially-destroyed types is that they can be safely destroyed without any additional cleanup operations. This ensure the discard self operation is safe.

In Swift official documents, there is a brief description to trivial types:

A trivial type can be copied bit for bit with no indirection or reference-counting operations. Generally, native Swift types that do not contain strong or weak references or other forms of indirection are trivial, as are imported C structs and enums. Copying memory that contains values of nontrivial types can only be done safely with a typed pointer. Copying bytes directly from nontrivial, in-memory values does not produce valid copies and can only be done by calling a C API, such as memmove().

I have tested many types and listed some below considered trivial types in Swift. It’s worth nothing that whether a type is considered trivial or not depends on variety of factors, including the size and complexity of the type. For example, even types that contain reference types (such as classes) or other non-trivial elements may be considered trivial if they meet certain criteria. Therefore, while the below list covers many of the most common cases, it is not exhaustive.

  • Builtin integer types (Int, UInt, Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64)
  • Builtin floating-point types (Float, Double, Float80)
  • Builtin Boolean type (Bool)
  • Raw pointer types (UnsafePointer, UnsafeMutablePointer, UnsafeRawPointer, UnsafeMutableRawPointer)
  • AnyOptional and its derivatives (such as Optional)
  • OptionSet types
  • Function types (() -> Void, (Int) -> String)
  • Tuples containing trivial types only
  • Enums that have no associated values or only have associated values of trivial types.
  • Structs containing stored properties of trivial types only

All trivially-destroyed types are also trivial types, the converse is not necessarily true. Conceptually, a trivial type is a type that has a simple structure and can be safely copied by simply copying its underlying memory and a trivial-destroyed type means whose instances can be destroyed without requiring any additional cleanup operations.

Now, we declare two general struct types for testing the consume operator:

1
2
3
4
5
6
struct TrivialStruct {
var age: Int = 100
}
struct NontrivialStruct {
var name: String = "csl"
}

When we use the consume to handle a nontrivial value, we will get expected behaviors as we comprehended in above learning.

1
2
3
var value = NontrivialStruct()
_ = consume value
print(value) // compilation error: `value` used after being consumed.

But if we replace the NontrivialStruct to the TrivialStruct, we will receive a compilation error: 'consume' applied to value that the compiler does not support checking. And if we also change the var to the let, the compilation error disappeared, but confusingly the value can still be used after being consumed.

1
2
3
var value = TrivialStruct()
_ = consume value // compilation error: 'consume' applied to value that the compiler does not support checking
print(value)
1
2
3
4
let value = TrivialStruct()
_ = consume value
// The value can still be accessed after being consumed.
print(value) // TrivialStruct(age: 100)

For understanding what underlying operations the compiler did, we can use the swiftc to generate canonical SIL code for our example code with the command swiftc main.swift -emit-silgen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// var value = NontrivialStruct()
// _ = consume value

// main()
sil hidden [ossa] @$s4mainAAyyF : $@convention(thin) () -> () {
bb0:
%0 = metatype $@thin NontrivialStruct.Type // user: %2
// function_ref NontrivialStruct.init()
%1 = function_ref @$s4main16NontrivialStructVACycfC : $@convention(method) (@thin NontrivialStruct.Type) -> @owned NontrivialStruct // user: %2
%2 = apply %1(%0) : $@convention(method) (@thin NontrivialStruct.Type) -> @owned NontrivialStruct // users: %12, %3
%3 = begin_borrow [lexical] %2 : $NontrivialStruct // users: %11, %5, %4
debug_value %3 : $NontrivialStruct, let, name "value" // id: %4
%5 = copy_value %3 : $NontrivialStruct // user: %6
%6 = move_value [allows_diagnostics] %5 : $NontrivialStruct // users: %10, %7
%7 = begin_borrow [lexical] %6 : $NontrivialStruct // users: %9, %8
debug_value %7 : $NontrivialStruct, let, name "another" // id: %8
end_borrow %7 : $NontrivialStruct // id: %9
destroy_value %6 : $NontrivialStruct // id: %10
end_borrow %3 : $NontrivialStruct // id: %11
destroy_value %2 : $NontrivialStruct // id: %12
%13 = tuple () // user: %14
return %13 : $() // id: %14
} // end sil function '$s4mainAAyyF'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// var value = TrivialStruct()
// _ = consume value

// main()
sil hidden @$s4mainAAyyF : $@convention(thin) () -> () {
bb0:
%0 = metatype $@thin TrivialStruct.Type // user: %2
// function_ref TrivialStruct.init()
%1 = function_ref @$s4main13TrivialStructVACycfC : $@convention(method) (@thin TrivialStruct.Type) -> TrivialStruct // user: %2
%2 = apply %1(%0) : $@convention(method) (@thin TrivialStruct.Type) -> TrivialStruct // users: %4, %3
debug_value %2 : $TrivialStruct, let, name "value" // id: %3
debug_value %2 : $TrivialStruct, let, name "another" // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function '$s4mainAAyyF'

As you can see above, a non-trivial value will do some operations about ownership and a trivial value is treaded as normal variables without any additional operations. So we can continually use the value even though it has already been consumed.

Then we redefine our NontrivialStruct and TrivialStruct to be noncopyable types and add empty consuming methods for them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct TrivialStruct: ~Copyable {
var age: Int = 100

consuming func consume() {}
deinit {
print("deinit age: \(age)")
}
}
struct NontrivialStruct: ~Copyable {
var name: String = "csl"

consuming func consume() {}
deinit {
print("deinit name: \(name)")
}
}

Whether we use let or var and NontrivialStruct or TrivialStruct, they all behaved expectedly. Then I found another question that we can still modify properties of the noncopyable variable having been consumed.

1
2
3
4
5
6
7
8
var value = TrivialStruct()
value.consume()

value.age = 200
value.consume()

// deinit age: 100
// deinit age: 200

Execute the above code, the console wil print deinit messages twice. This is because modifying a struct will trigger the copy-on-write to duplicate a new instance. But if the TrivialStruct is of normal copyable types, the compiler doesn’t allow us to modify properties of the variable being consumed by the syntax consume value.

Even if we don’t invoke the method value.consume(), we can also see twice deinit messages. The funny thing is that if we declare the struct with more than one properties, we will see some strange behaviors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct SingleStruct: ~Copyable {
var age: Int = 100

consuming func consume() {}
deinit {
print("single deinit age: \(age)")
}
}

struct MultipleStruct: ~Copyable {
var age: Int = 100
var weight: Float = 80

consuming func consume() {}
deinit {
print("multiple deinit age: \(age)")
}
}
1
2
3
4
5
6
7
func main() {
var single = SingleStruct()
var multiple = MultipleStruct()
}

// single deinit age: 100
// multiple deinit age: 100

Just create instances and then their deinit methods will be invoked once the calling stack ended.

1
2
3
4
5
6
7
8
9
func main() {
var single = SingleStruct()
print(single.age)

var multiple = MultipleStruct()
print(multiple.age)
}

// single deinit age: 100

The variable multiple having two properties didn’t invoke its deinit method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var single = SingleStruct()
single.consume()
single.age = 999
// single.consume() // ok

var multiple = MultipleStruct()
multiple.consume()
multiple.age = 999
// multiple.consume() // compilation error.
}

// single deinit age: 100
// single deinit age: 999
// multiple deinit age: 100

The deinit method invoked when invoking the consume method. The variable multiple didn’t invoke the deinit method again after modifying its property age.

1
2
3
4
5
6
7
8
9
10
11
func main() {
var single = SingleStruct()
single.consume()
single.age = 999
single.consume() // ok

var multiple = MultipleStruct()
multiple.consume()
multiple.age = 999
multiple.consume() // compilation error.
}

The compiler threw an error when we tried to invoke the consume method after reassigning the property age. If we reassign all properties of the consumed variable and compile again, we will get expected output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var single = SingleStruct()
single.consume()
single.age = 999
single.consume() // ok

var multiple = MultipleStruct()
multiple.consume()
multiple.age = 999
multiple.weight = 300
multiple.consume()
}

// single deinit age: 100
// single deinit age: 999
// multiple deinit age: 100
// multiple deinit age: 999

Finally, I proposed a assumption that a noncopyable variable with multiple stored properties must reassign all its properties for completing initialization after being consumed. A consumed variable with multiple properties will not invoke its deinit method after properties are changed unless invoking the consume method again.