User defined initializers always call member initializers

Following the discussion of SE-0242, it has been brought up that user defined initializers implicitly call all the member initializers for the given struct/class and that this is considered a bug.

Bug here: [SR-10092] Custom initializers always call member initializers · Issue #52494 · apple/swift · GitHub

Given:

func zero() -> Int {
  print("hello")
  return 0
}

struct X {
  var y = zero()

  init(y: Int) {
    self.y = y
  }
}

let x = X(y: 10) // hello

Calling the user defined initializer surprisingly invokes this side effect, but it was never called in the init.

If we look at the lowered init in that example, it looks something like:

init(y: Int) {
  // Inserted here to ensure that y is initialized.
  self.y = zero()
  self.y = y
}

This gets complicated pretty quickly because if we decide to just remove the member init then we could have source compatibility issues where they never initialized the property in the first place. i.e.

init(y: Int) {} // does this now error?

or do we check to ensure that the property is initialized first, and then deleted? If so, how would the following behave:

init(y: Int) {
  if y > 0 {
    self.y = y // ok, this is initialized now.
  } else {
    // do we initialize self.y here?
    print(self.y) // not initialized yet... do we error here?
  }

  // do we initialize self.y here just in case?
  print(self.y) // could be initialized, could not be... do we error here?
}

This is most definitely going to require a proposal of some sort, but to me what makes the most sense is to only call the member initializers in the default initializer and produce errors in user defined ones (this is almost guaranteed source breaking).

func zero() -> Int {
  print("hello")
  return 0
}

struct Foo {
  var y = zero()

  // currently the following is legal...
  // error: return from initializer without initializing all stored properties
  init(y: Int) {
    // self.y = y
  }
}

struct Bar {
  var y = zero()
}

let foo = Foo(y: 10) // nothing is printed
let bar = Bar() // hello

cc some people who were a part of that discussion: @jrose @xwu @anandabits @nevin (apologies if there was someone I missed).

1 Like

My first inclination is to say that within an initializer, for each stored property, either a value must be assigned to that property on every code-path, or on no code-path (in which case the initial value is used). This would also be potentially source-breaking, but only for initializers that have some test to decide whether or not to use an initial value.

This is another option I explored, but it feels awkward giving initializers more rules and behaviors which might make it harder to teach. i.e. this feels weird:

init(x: Int) {
  // ok, no code path assigns 'x', emit the initial value
  print(self.x) // ok
}

// vs.

init(x: Int) {
  // ok, we assign 'x', don't emit initial value
  print(self.x) // error: 'self' used before all stored properties are initialized
  self.x = x
}

(That's not to say that the current initializer behavior everywhere is pretty complicated already to teach).

A third option would be, “If there exists at least one code-path in the initializer which does not assign a value to some stored property, then that property is assigned its initial value at the start of the initializer.”

Of course the ideal solution would be something like, “At every line of an initializer, for each stored property, either that property must be assigned a value on every code-path to that line, or on no code-path to that line.”

That combined with “Every stored property must be assigned a value before leaving the initializer or using the instance” and “Evaluate initial values as late as possible”, should do what we want.

I disagree that this is a bug; it is a major change of behavior, one that complicates initializer lowering considerably, and I think the outcome will be worse overall. More details here: SE-0242: Synthesize default values for the memberwise initializer - #101 by Alejandro

If the core team feels that this is a desirable change, it needs an evolution proposal.

1 Like