didSet is called on property mutation

40 views
Skip to first unread message

Jens Alfke

unread,
Jun 2, 2015, 6:11:39 PM6/2/15
to swift-l...@googlegroups.com
I’ve run into something unexpected: if I add a didSet block to the definition of a struct property, it’s called when a mutating method is called on the current property value, not just when a new value is assigned. In other words, “c.foo.doSomething()”, where doSomething() is a mutating method, will invoke a didSet block for the property c.foo. There’s no mention of this in the Swift book, as far as I can tell.

Here’s an example:

import Foundation

struct Foo : Printable {
    private var x: Int = 0
    mutating func add(n: Int) {
        x += n
    }
    var description: String {
        return "Foo(\(x))"
    }
}

struct Container {
   var foo = Foo() {
        didSet {
            println("didSet foo! was \(oldValue) now \(foo)")
        }
    }
}

var c = Container()
println(c.foo)
c.foo.add(10)
println(c.foo)

The output when run is:
Foo(0)
didSet foo! was Foo(0) now Foo(10)
Foo(10)

The weirdest part is that the didSet block has access to the struct in both its old and new state. In other words, it looks like the struct is copied before the mutating method is called, so that the didSet block can be passed the copy as its ‘oldValue’ parameter. Now I’m curious whether this behavior happens only if a didSet (or willSet) block is present, or if structs are always mutated this way. It could get expensive to make copies all the time, especially if the struct has references to class objects whose refcounts need to be adjusted.

—Jens

Chris Lattner

unread,
Jun 2, 2015, 8:05:15 PM6/2/15
to Jens Alfke, swift-l...@googlegroups.com
On Jun 2, 2015, at 3:11 PM, Jens Alfke <je...@mooseyard.com> wrote:

I’ve run into something unexpected: if I add a didSet block to the definition of a struct property, it’s called when a mutating method is called on the current property value, not just when a new value is assigned. In other words, “c.foo.doSomething()”, where doSomething() is a mutating method, will invoke a didSet block for the property c.foo. There’s no mention of this in the Swift book, as far as I can tell.

Hi Jens,

This is an intentional, and very important part of the model.  I explore this area in a bit more detail here:

-Chris


Here’s an example:

import Foundation

struct Foo : Printable {
    private var x: Int = 0
    mutating func add(n: Int) {
        x += n
    }
    var description: String {
        return "Foo(\(x))"
    }
}

struct Container {
   var foo = Foo() {
        didSet {
            println("didSet foo! was \(oldValue) now \(foo)")
        }
    }
}

var c = Container()
println(c.foo)
c.foo.add(10)
println(c.foo)

The output when run is:
Foo(0)
didSet foo! was Foo(0) now Foo(10)
Foo(10)

The weirdest part is that the didSet block has access to the struct in both its old and new state. In other words, it looks like the struct is copied before the mutating method is called, so that the didSet block can be passed the copy as its ‘oldValue’ parameter. Now I’m curious whether this behavior happens only if a didSet (or willSet) block is present, or if structs are always mutated this way. It could get expensive to make copies all the time, especially if the struct has references to class objects whose refcounts need to be adjusted.

—Jens

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to swift-languag...@googlegroups.com.
To post to this group, send email to swift-l...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/02D3D5F8-8A61-48BA-B145-2F0B748E99B5%40mooseyard.com.
For more options, visit https://groups.google.com/d/optout.

Jens Alfke

unread,
Jun 3, 2015, 5:49:53 PM6/3/15
to Chris Lattner, swift-l...@googlegroups.com

On Jun 2, 2015, at 5:05 PM, Chris Lattner <clat...@apple.com> wrote:

This is an intentional, and very important part of the model.  I explore this area in a bit more detail here:

That’s very interesting, and definitely deserves calling out in the book — the actual semantics of ‘inout’ and the unary ‘&’ operator seem different than what most people would expect.

As an inveterate cycle-counter, I’m concerned about the cost of implementing mutating methods this way. Copying the entire struct (twice) is more expensive than operating on it in place, particularly as the size of the struct grows. Does the copying always occur, or are there situations where it can be optimized away?

—Jens

Chris Lattner

unread,
Jun 3, 2015, 6:53:51 PM6/3/15
to Jens Alfke, swift-l...@googlegroups.com
The inout copy in the callee is almost always eliminated.  The only place is remains is when captured by a (non-@noescape) closure that isn’t otherwise optimized away (e.g. by being inlined).

-Chris

Chris Lattner

unread,
Jun 3, 2015, 6:56:19 PM6/3/15
to Jens Alfke, swift-l...@googlegroups.com
One additional thing: The semantic model here is very similar to local variables.  Semantically (and worst case) all local variables are heap allocated:

  func foo() {
     var x = 42 // semantically this is on the heap
  }

however, this is only visible when closed over by an escaping closure.  In the vastly most common cases, these are actually put on the stack according to the standard “as if” rule that compilers use.

Also, FWIW, these things are even done at -O0.

-Chris

Jens Alfke

unread,
Jun 3, 2015, 7:42:00 PM6/3/15
to Chris Lattner, swift-l...@googlegroups.com

On Jun 3, 2015, at 3:53 PM, Chris Lattner <clat...@apple.com> wrote:

The inout copy in the callee is almost always eliminated.  The only place is remains is when captured by a (non-@noescape) closure that isn’t otherwise optimized away (e.g. by being inlined).

…or when the receiver of the mutating method is a property of another object, right? Since that’s the case I ran into. There’s definitely copying going on in that case, because my didSet block gets passed both the old and the new value of the struct.

—Jens

Chris Lattner

unread,
Jun 3, 2015, 7:51:36 PM6/3/15
to Jens Alfke, swift-l...@googlegroups.com
That’s a different case, and you’re right that that is not always eliminated (please file a radar with a small testcase if you can though).

The case I was referring to is:

func f<T>(inout a : T) {
   do_closure_thing { a }
}

In this case, a copy has to be made callee side of f, because the closure could extend the lifetime of the thing passed in.  It is surprising to many folks, but the name “inout” was chosen because the semantics of the code above are:

func f<T>(inout tmp : T) {
   var a = tmp    // "in"
   do_closure_thing { a = whatever() }
   tmp = a     // "out"
}

which can surprise people because mutations to the closed over local variable only get seen in the caller if they happen before “f" returns.

-Chris


Matthias Zenger

unread,
Jun 7, 2015, 9:05:41 PM6/7/15
to swift-l...@googlegroups.com, je...@mooseyard.com
Chris, I'm confused now. I just started to read the Swift language manual and found the section on "inout" parameters totally under-specified. So, I did a little bit of experimentation. The first code I wrote was along the lines of this:

var global = 0


func foo(inout x: Int) -> (Int, Int, Int) {

 func inc() -> Int {

   return ++x

 }

 return (global, inc(), global)

}


foo(&global)    // (0, 1, 1)
global          // 1


The result of foo(&global) is (0, 1, 1), which indicates that this is, in fact, call by reference, because ++x mutates both x and global simultaneously. Motivated by your post, I inserted an assignment into foo which stores inc in global variable f:


var global = 0

var f = { 0 }


func foo(inout x: Int) -> (Int, Int, Int) {

 func inc() -> Int {

   return ++x

 }

 f = inc

 return (global, inc(), global)

}


foo(&global)    // (0, 1, 0)
global          // 1


It was a total surprise for me to see that just by adding an innocent assignment, which shouldn't impact the computation at all, the result of the function call foo(&global) changed to (0, 1, 0). So, here, the "inout" parameter has real "inout semantics" in that it assigns the result of ++x to global only when the function returns to the client.


Am I right in concluding that the Swift compiler currently incorrectly optimizes cases where the inout parameter isn't captured by a closure that escapes the scope by simply using call by reference?


== Matthias

Reply all
Reply to author
Forward
0 new messages