Sub-class conformance ignored when reference to instance is casted to super class type

Given this example:

protocol ConformSomething {
    func whoAmI()
}

extension ConformSomething {
    func whoAmI() {
        print("Default implementation")
    }
}

class BaseClass: ConformSomething {
}

final class SubClass: BaseClass {
    func whoAmI() {
        print("SubClass")
    }
}

let x: BaseClass = SubClass()
x.whoAmI() // prints "Default implementation"

Why does the whoAmI() method on the concrete SubClass instance not get called and instead the default conformance is called instead?

Yet the same example using inheritance alone produces the expected (opposite) result:

class BaseClass {
    func whoAmI() {
        print("Default implementation")
    }
}

final class SubClass: BaseClass {
    override func whoAmI() {
        print("SubClass")
    }
}

let x: BaseClass = SubClass()
x.whoAmI() // prints "SubClass"

I'm a bit baffled to be honest, if I add the protocol conformance to the sub-class types, it says "''SubClass' inherits conformance to protocol 'ConformSomething' from superclass here".

So how on earth do you get access to the sub-classes whoAmI() methods when the variable reference to the instance is typed BaseClass?

One might say that the compiler doesn't know if all the sub-classes will implement the whoAmI() method so the compiler just uses the base conformance but of course they all inherit from the super-class anyway so they are guaranteed to have at least a default conformance and when using inheritance alone clearly the compiler is able to do this.

Conformance to a protocol is captured and frozen at compile time. If the Swift compiler doesn't see a requirement at compile time, it won't either at runtime.

Since you are declaring conformance of BaseClass to ConformSomething, the compiler does not see or "witness" an eligible implementation for the method requirement whoAmI() on the BaseClass. So it generates a protocol witness table for BaseClass that says there is no implementation for whoAmI(). As a result, method call through an ConformSomething existential would call the default implementation, because the dispatch in this case is done through the protocol witness table generated at compile time, which recorded "no implementation, call default".

For calls on existentials to hit the SubClass, the only way in today's Swift is to implement whoAmI() on the BaseClass. Only when the compiler does record in the protocol witness table that BaseClass has the method whoAmI(), then calls to whoAmI() through an ConformSomething existential can hit the class vtable and in turn your subclass' overriding implementation.

protocol P { func foo() { print("default impl") } }

class Base: P {
    func foo() { print("base impl") }
}

class Derived: Base {
    override func foo() { print("derived impl") }
}

let value = Derived()
value.foo() // OUTPUT: derived impl
(value as P).foo() // OUTPUT: derived impl
2 Likes

Thanks for the reply, was this a deliberate decision or just an acceptable side-effect of the way protocols are implemented?

Just seems hard to discover for the average Swift developer and I guess others would make the assumptions I did about how this works given the inheritance example I gave and am used to. Most developers don't have a deep knowledge of how the compiler and run-time works and don't directly interact with them.

Can you recommend some learning material as I would like to understand what happens under the hood to gain a better understanding of some of the language decisions?

https://bugs.swift.org/browse/SR-103

2 Likes

Yeah, this is one of the cases that is not designed to "hard to misuse". Static dispatch (baking the exact function call into the binary) is faster than dynamic dispatch (deciding which function to call depending on the class of the object at runtime), so it's not an easy thing to fix. Not to mention "fixing" it would cause already written code to behave differently, which is always a headache.

Here are some similar examples:

protocol ConformSomething {
    func whoAmI()
}

extension ConformSomething {
    func whoAmI() {
        print("Default implementation")
    }
}

class BaseClass: ConformSomething {
}

final class SubClass: BaseClass {
    func whoAmI() {
        print("SubClass")
    }
}

let x: ConformSomething = SubClass()
x.whoAmI() // prints "SubClass"
protocol ConformSomething {
}

extension ConformSomething {
    func whoAmI() {
        print("Default implementation")
    }
}

class BaseClass: ConformSomething {
}

final class SubClass: BaseClass {
    func whoAmI() {
        print("SubClass")
    }
}

let x: ConformSomething = SubClass()
x.whoAmI() // prints "Default implementation"
1 Like