Odd Property Wrapper Memberwise initializer behavior

@propertyWrapper
struct Tagged<Value> {
    var wrappedValue: Value

    init(wrappedValue: Value, name: String) {
        self.wrappedValue = wrappedValue
    }

    init(name: String) {
        fatalError("init without wrapped value")
    }
}

struct Foo {
    @Tagged(name: "A") var a: Int
}

Foo(a: .init(wrappedValue: 1, name: "A")) // OK!!

Surprisingly works. The memberwise initializer skips the implicit initialization of Tagged(name: "A") and directly assigns _a = Tagged(wrappedValue: 1, name: "A").

However, it doesn't seem possible to skip the implicit property wrapper initialization once you provide your own init.

struct Bar {
    @Tagged(name: "A") var a: Int
    
    init(a: Tagged<Int>) {
        // implicit Tagged(name: "A") will trap

        self._a = a
    }
}

Bar(a: .init(wrappedValue: 1, name: "A")) // trap

This trap maybe seems consistent with the spec outlined in Swift Evolution.

Does this seem odd to anyone else or is this the expected behavior?

Related, I do wish it was possible to require a property wrapper with additional arguments always be initialized with an initial value.

By "trap" you mean that it does not compile? Seems correct to me as Bar has an explicit init now and the synthesized one is gone.

He means the call to fatalError() is reached. I played around in a Playground and the behavior seems very odd to me.

Oh, wait the explicit init takes Tagged, brain fade.

The last example should just work. Iā€˜ll test it later when I get the chance to fire up my mac.

If I'm using godbolt correctly, the Bar.init compiles to:

Assembly
output.Bar.init(a: output.Tagged<Swift.Int>) -> output.Bar:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48
        mov     qword ptr [rbp - 8], 0
        mov     qword ptr [rbp - 16], 0
        mov     qword ptr [rbp - 16], rdi
        lea     rax, [rip + .L__unnamed_1]
        mov     esi, 1
        mov     edx, 1
        mov     qword ptr [rbp - 32], rdi
        mov     rdi, rax
        call    ($sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC)@PLT
        mov     rsi, qword ptr [rip + ($sSiN)@GOTPCREL]
        lea     rdi, [rbp - 24]
        mov     qword ptr [rbp - 40], rax
        mov     rax, rdi
        mov     rdi, qword ptr [rbp - 40]
        mov     qword ptr [rbp - 48], rsi
        mov     rsi, rdx
        mov     rdx, qword ptr [rbp - 48]
        call    (output.Tagged.init(name: Swift.String) -> output.Tagged<A>)
        mov     rax, qword ptr [rbp - 24]
        mov     qword ptr [rbp - 8], rax
        mov     rax, qword ptr [rbp - 32]
        mov     qword ptr [rbp - 8], rax
        add     rsp, 48
        pop     rbp
        ret

I'm no expert, but it seems to be creating an instance of Tagged, even though it should only be a simple property assignment.

The same behavior happens without property wrappers:

struct Tagged<Value> {
    var wrappedValue: Value

    init(wrappedValue: Value, name: String) {
        self.wrappedValue = wrappedValue
    }

    init(name: String) {
        fatalError("init without wrapped value")
    }
}

struct Foo {
    var a: Tagged<Int> = Tagged(name: "A")
}

Foo(a: .init(wrappedValue: 1, name: "A")) // OK!!

struct Bar {
    var a: Tagged<Int> = Tagged(name: "A")

    init(a: Tagged<Int>) {
        // implicit Tagged(name: "A") will trap

        self.a = a
    }
}

Bar(a: .init(wrappedValue: 1, name: "A")) // trap

simplified example, showing that test is assigned to twice

struct Bar {
    var test: Void = print(1)
    init() { test = print(2) }
}
Bar()

will print both 1 and 2

I think the following as more analogous to the original:

struct Bar {
    var test: Void = print(1)
    init(test v: Void) { test = v }
}
Bar(test: print(2))

struct Foo {
    var test: Void = print(1)
}
Foo(test: print(2))

as it more clearly shows that the struct with an implicit init behaves in a surprisingly different manner with respect to member initialization.

The output is:

2
1
2
1 Like

I might've gone a step too far with my simplifying :sweat_smile:

So is this a bug or expected? :thinking:

I think it's a bug, it was mentioned in SE-0242's acceptance.

3 Likes

Uff. Good to know. I need to watch out for this.

The original issue can be solved though:

struct Bar {
  @Tagged // drop `(name: "A")` here
  var a: Int

  init(a: Tagged<Int>) {
    self._a = a
  }
}

Wow, I wouldn't have guessed it was general to all memberwise initializers. Good to know it's at least mentioned in that SE-0242 thread.

Consider this issue explained. I have some related but property wrapper questions to bring up in a separate thread.

Thanks!

Terms of Service

Privacy Policy

Cookie Policy