[Pitch] Consistently prohibiting classes with missing required initializers

Hi all,

As reported in issue #69965, Swift currently has a hole in it's checking to ensure that a class has initializers. If one writes the following:

class MySuperclass {
  required init() { }
  init(boop: Bool) {}
}

class MySubclass: MySuperclass {
    var hi: String
}

the Swift compiler will reject it with:

error: class 'MySubclass' has no initializers

However, if you put MySuperclass into one module and MySubclass into a different module, like this:

// module A
open class MySuperclass {
  required public init() { }
  internal init(boop: Bool) {}
}

// module B
import A
class MySubclass: MySuperclass {
    var hi: String
}

there is no error at all---meaning that one can end up creating MySubclass instances via the required MySuperclass.init() initializer without having initialized hi.

There are three points at which we could have rejected the code:

  1. MySubclass lacks the required init(), which we must diagnose
  2. MySubclass has no initializers whatsoever, which we could diagnose to be consistent
  3. MySuperclass is marked open but cannot be subclassed outside of the module due to the internal designated initializer init(boop:), so we could reject the open.

(1) is clearly a bug fix---we have a rule for required initializers, and we are not enforcing it consistently.

(2) is likely a bug fix---there's nothing in the language model that makes this rule less reasonable across modules. As far as I can tell, the carve-out that suppressed the diagnostic was introduced for Objective-C interoperability, because an Objective-C designated initializer might not be importable into Swift, and that Swift classes accidentally got through. We'd likely have to leave the hole in place for classes defined in Objective-C.

(3) is adding a new restriction to open, so it doesn't feel like a bug fix. However, it does move the error to the point where the mistake was initially made---an open class that can't be subclassed is almost certainly a mistake. I can't think of any reason why we wouldn't want to reject it.

Thoughts on whether we should introduce the restriction on open described by (3)?

Doug

7 Likes

I thought subclasses only have to override all designated inits if they want to inherit convenience inits? (And that @_hasMissingDesignatedInitializers indicates the presence of a non-public designated init to outside subclasses, so they know they can’t inherit?)

Or should the internal init also be required? That would prevent subclassing, I suppose.

2 Likes

Can’t you hypothetically subclass MySuperclass and pass MySubclass.self to some function belonging to the MySuperclass module, where it will be instantiated? That would let you override public methods, but not to add fields or change the initializer.

2 Likes

They also need to override all designated inits if they have stored properties with an initial value.

Yes, that's true. I guess this means that we shouldn't do (3) at all.

FWIW, I implemented (1) and (2) in Fix missing diagnostics about the lack of initializers in cross-module inheritance scenarios by DougGregor · Pull Request #70219 · swiftlang/swift · GitHub, since those are bug fixes.

Doug

1 Like

What’s supposed to happen if you give MySubclass.hi a default value? Will the synthesized MySuperclass.init() correctly initialize MySubclass.hi?

1 Like

And are there optimizations assuming this can’t happen that might need to be changed?

1 Like

Yes.

I can't think of any such optimizations.

Doug

1 Like

Main thing I can think of is “this initializer isn’t open, and there are no overrides in the module, therefore I can devirtualize dynamic calls using it”.

1 Like

Why is that? Why wouldn’t it be adequate to override all visible designated inits, and then not inherit the invisible ones?

Like, module A already shouldn’t be assuming that subclasses of MySuperclass have an init(boop:), if only because a subclass with no initial values might not have one. And B shouldn’t be assuming that MySubclass has overridden all of MySuperclass’s designated inits because it’s been marked to indicate otherwise.

I feel like I’m missing something obvious, but for the life of me I can’t figure out what it is.

4 Likes

Initializers can't be open. The check would need to be in terms of whether the class itself is open, so I think it would be okay.

Oh my, I have a load-bearing typo in the bit you quoted. I was trying to say that if there are any stored properties without an initial value, you can't get the initializers in your subclass synthesized for you.

No, I think it's me who is missing something. The rules:

  • If all designated inits have been overridden, you inherit convenience initializers.
  • If there are designated inits not visible to you, there is no way to inherit convenience initializers.

I incorrectly conflated "no way to inherit convenience initializers" with "no way to initialize this", which is absolutely not true: one can use any of the public designated initializers to initialize the superclass.

So my (3) is actively wrong. Thank you for the correction!

Doug

6 Likes