Working around restrictions on convenience initializers

i have a base class with a private memberwise init, and a public convenience init:

public
class Base 
{
    let a:[Int]
    let b:[Int]
    let c:[Int]
    
    private
    init(a:[Int], b:[Int], c:[Int])
    {
        self.a = a 
        self.b = b
        self.c = c
    }

    public convenience required
    init(x:[Int])
    {
        ...
        self.init(a: a, b: b, c: c)
    }
    
    public 
    func f()
    {
        ...
    }
}

now i want to define subclasses that adds no stored properties but overrides some of the virtual class members. however, this requires duplicating the implementation of init(x:), since a subclass init cannot call a superclass convenience init.

public final 
class Subclass:Base
{
    public required
    init(x:[Int])
    {
        ...
        super.init(a: a, b: b, c: c)
    }

    public override 
    func f()
    {
        ...
    }
}

one way to to satisfy the compiler error is to make the superclass init(x:) a designated initializer. however a designated init cannot call another designated init, which means it cannot delegate to the memberwise init, instead it must assign to the instance properties from within the body of the complex init.

are there any better ways to work around this restriction?

1 Like

Not an answer below (I believe Swift can't do this but I'd love to be mistaken).

Is this checked? I don't see it is:

public class Base {
    let a, b, c: [Int]
    private init(a: [Int], b: [Int], c: [Int]) {
        self.a = a; self.b = b; self.c = c
    }
    public convenience required init(x: [Int]) {
        self.init(a: x, b: x, c: x)
    }
    public func f() {}
}
public final class Subclass: Base {
    public override func f() {}
}

let x = Subclass(x: [])
print(x) // Base !?!

Note that this triggers a serious foot-gun in the language... I'd rather see a compilation error here (if there's no better way), or, it somehow "works correctly" i.e. prints "Subclass".

This looks like a bug.

1 Like

could the reflection description also be related to miscompile: assigning a subclass instance to existential causes base class methods to be called instead of subclass methods · Issue #73962 · apple/swift · GitHub ?

1 Like

I don’t think it’s a problem in reflection per se, it’s literally instantiating the wrong class. Yes, it could be a similar problem although the latter appears to be an optimizer bug. I’ll try to take a look.

1 Like

The former is also -O only, BTW.


Edit: adding a crashing scenario. Same as above plus:

public final class Subclass: Base {
    var y: String = "Hello, World! 123"
    public override func f() {}
}

let x = Subclass(x: [])
print(x) // Base in `-O` !?! 
print(x.y) // 💣 CRASH
1 Like

It's the same bug. I'm working on a fix (Sema: Ban unsound convenience initializer inheritance inside a module by slavapestov · Pull Request #73988 · swiftlang/swift · GitHub).

Looks like I introduced the bug in Swift 5.1 with SILGen: Correctly emit vtables when an override is more visible than the base by slavapestov · Pull Request #25137 · swiftlang/swift · GitHub. This fixed linker errors when you did the invalid thing across module boundaries (and a proper diagnostic was then added in [ModuleInterface][5.2] Add Support for @_hasMissingDesignatedInitializers by CodaFi · Pull Request #29027 · swiftlang/swift · GitHub), but the behavior within a module silently regressed since. Sorry about that.

3 Likes

no worries, happens to the best of us!