Does a mutating struct function in swift create a new copy of self?
Now does the existing struct in memory get mutated, or is self replaced with a new instance
Conceptually, these two options are exactly the same. I'll use this example struct, which uses UInt8 instead of Double (because its bits are easier to visualize).
struct Point {
var x: UInt8
var y: UInt8
mutating func add(x: UInt8){
self.x += x
}
}
and suppose I create a new instance of this struct:
var p = Point(x: 1, y: 2)
This statically allocates some memory on the stack. It'll look something like this:
00000000 00000001 00000010 00000000
<------^ ^------^ ^------^ ^----->
other | self.x | self.y | other memory
^----------------^
the p struct
Let's see what will happen in both situations when we call p.add(x: 3)
:
The existing struct is mutated in-place:
Our struct in memory will look like this:
00000000 00000100 00000010 00000000 <------^ ^------^ ^------^ ^-----> other | self.x | self.y | other memory ^----------------^ the p struct
Self is replaced with a new instance:
Our struct in memory will look like this:
00000000 00000100 00000010 00000000 <------^ ^------^ ^------^ ^-----> other | self.x | self.y | other memory ^----------------^ the p struct
Notice that there's no difference between the two scenarios. That's because assigning a new value to self causes in-place mutation. p
is always the same two bytes of memory on the stack. Assigning self a new value to p
will only replace the contents of those 2 bytes, but it'll still be the same two bytes.
Now there can be one difference between the two scenarios, and that deals with any possible side effects of the initializer. Suppose this is our struct, instead:
struct Point {
var x: UInt8
var y: UInt8
init(x: UInt8, y: UInt8) {
self.x = x
self.y = y
print("Init was run!")
}
mutating func add(x: UInt8){
self.x += x
}
}
When you run var p = Point(x: 1, y: 2)
, you'll see that Init was run!
is printed (as expected). But when you run p.add(x: 3)
, you'll see that nothing further is printed. This tells us that the initializer is not anew.
I did this:
import Foundation
struct Point {
var x = 0.0
mutating func add(_ t:Double){
x += t
}
}
var p = Point()
withUnsafePointer(to: &p) {
print("\(p) has address: \($0)")
}
p.add(1)
withUnsafePointer(to: &p) {
print("\(p) has address: \($0)")
}
and obtained in output:
Point(x: 0.0) has address: 0x000000010fc2fb80
Point(x: 1.0) has address: 0x000000010fc2fb80
Considering the memory address has not changed, I bet the struct was mutated, not replaced.
To replace completely something, you have to use another memory address, so it's pointless to copy back the object in the original memory address.
I feel it's worth taking a look (from a reasonably high-level) at what the compiler does here. If we take a look at the canonical SIL emitted for:
struct Point {
var x = 0.0
mutating func add(_ t: Double){
x += t
}
}
var p = Point()
p.add(1)
We can see that the add(_:)
method gets emitted as:
// Point.add(Double) -> ()
sil hidden @main.Point.add (Swift.Double) -> () :
$@convention(method) (Double, @inout Point) -> () {
// %0 // users: %7, %2
// %1 // users: %4, %3
bb0(%0 : $Double, %1 : $*Point):
// get address of the property 'x' within the point instance.
%4 = struct_element_addr %1 : $*Point, #Point.x, loc "main.swift":14:9, scope 5 // user: %5
// get address of the internal property '_value' within the Double instance.
%5 = struct_element_addr %4 : $*Double, #Double._value, loc "main.swift":14:11, scope 5 // users: %9, %6
// load the _value from the property address.
%6 = load %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %8
// get the _value from the double passed into the method.
%7 = struct_extract %0 : $Double, #Double._value, loc "main.swift":14:11, scope 5 // user: %8
// apply a builtin floating point addition operation (this will be replaced by an 'fadd' instruction in IR gen).
%8 = builtin "fadd_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %9
// store the result to the address of the _value property of 'x'.
store %8 to %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // id: %9
%10 = tuple (), loc "main.swift":14:11, scope 5
%11 = tuple (), loc "main.swift":15:5, scope 5 // user: %12
return %11 : $(), loc "main.swift":15:5, scope 5 // id: %12
} // end sil function 'main.Point.add (Swift.Double) -> ()'
(by running xcrun swiftc -emit-sil main.swift | xcrun swift-demangle > main.silgen
)
The important thing here is how Swift treats the implicit self
parameter. You can see that it's been emitted as an @inout
parameter, meaning that it'll be passed by reference into the function.
In order to perform the mutation of the x
property, the struct_element_addr
SIL instruction is used in order to lookup its address, and then the underlying _value
property of the Double
. The resultant double is then simply stored back at that address with the store
instruction.
What this means is that the add(_:)
method is able to directly change the value of p
's x
property in memory without creating any intermediate instances of Point
.