Whose witness table should be used?
Asked Answered
C

1

7

I want to have a solid understanding of method dispatching in Swift. I read from this popular blog about the three types of dispatches as below:

  1. Dynamic
  2. Table(Witness table in Swift)
  3. Message

In that blog the author says that the NSObject subtypes maintain a dispatch table(witness table) and also a message dispatch hierarchy. The code snippet shared by the author is as below:

class Person: NSObject {
    func sayHi() {
        print("Hello")
    }
}
func greetings(person: Person) {
    person.sayHi()
}

greetings(person: Person()) // prints 'Hello'

class MisunderstoodPerson: Person {}
extension MisunderstoodPerson {
    override func sayHi() {
        print("No one gets me.")
    }
}
greetings(person: MisunderstoodPerson()) // prints 'Hello'

I will quote the author's reasoning for the call sayHi() on an instance of Person:

The greetings(person:) method uses table dispatch to invoke sayHi(). This resolves as expected, and “Hello” is printed. Nothing too exciting here. Now, let’s subclass Person

The the author goes on to explain the call sayHi() on an instance of MisunderstoodPerson typecast to Person:

Notice that sayHi() is declared in an extension meaning that the method will be invoked with message dispatch. When greetings(person:) is invoked, sayHi()is dispatched to the Person object via table dispatch. Since the MisunderstoodPerson override was added via message dispatch, the dispatch table for MisunderstoodPerson still has the Person implementation in the dispatch table, and confusion ensues.

I want to know how the author reached to the conclusion that the greetings(person:) method uses table dispatch to invoke sayHi()

One thing that author mentions earlier in the blog is when an NSObject subclass declares a method in the initial declaration(meaning not in extension) a table dispatch would be used.

So I assume that as the parameter 'person' type is Person for the greetings(person:) method and the method called is sayHi() which is declared in the initial declaration of the Person class a table dispatch is used and sayHi() from Person is called. Its safe to say Person witness table is used.

Once we have subclass MisunderstoodPerson and pass this instance to greetings(person:) this instance should be treated as Person. I have a confusion here and have couple of questions here.

  • The instance is of type MisunderstoodPerson so would witness table of MisunderstoodPerson be used here.
  • Or the instance has been typecast to Person, so would witness table of Person be used here.

The author does not clarify this in the blog as to whose witness tables should be used. Even in some cases I get a feel that author describes even compilers creating witness tables for protocols. It is evident from the image he shares in the blog which is as below.

enter image description here I would be really grateful, If some one could explain it.

Chiekochien answered 4/4, 2019 at 6:5 Comment(0)
S
4

That blog post is a bit outdated in that inheriting from NSObject no longer changes the dispatch behaviour of a class (in Swift 3, it would cause members to be implicitly exposed to Obj-C, which would change dispatch behaviour of extension members, but this is no longer the case). The example they give also no longer compiles in Swift 5, as you can only override a dynamic member from an extension.

In order to distinguish static dispatch from dynamic dispatch, let's consider protocols separately. For protocols, dynamic dispatch is used if both:

  • The member is declared within the main protocol body (this is called a requirement or customisation point).
  • The member is called on a protocol-typed value P, protocol composition typed value P & X, or generic placeholder typed value T : P, for example:

    protocol P {
      func foo()
    }
    
    struct S : P {
      func foo() {}
    }
    
    func bar(_ x: S) {
      x.foo() // Statically dispatched.
    }
    
    func baz(_ x: P) { 
      x.foo() // Dynamically dispatched.
    }
    
    func qux<T : P>(_ x: T) { 
      x.foo() // Also dynamically dispatched.
    }
    

If the protocol is @objc, message dispatch is used, otherwise table dispatch is used.

For non-protocol members, you can ask the question: "Can this be overriden?". If the answer is no, you're looking at static dispatch (e.g a struct member or final class member). If it can be overriden, you're looking at some form of dynamic dispatch. However it's worth noting that if the optimiser can prove that it isn't overriden (e.g if it's fileprivate and not overriden within that file), then it can be optimised to use static dispatch.

For a normal method call, the dynamic modifier is what distinguishes the two current forms of dynamic dispatch, table dispatch and Obj-C message dispatch. If a member is dynamic, Swift will use message dispatch. As stated, this rule seems pretty straightforward, however some members are not explicitly marked dynamic – the compiler infers it instead. This includes:

A lesser known form of method call in Swift is the dynamic method call, which is done by accessing an @objc member on an AnyObject value. For example:

import Foundation

class C {
  @objc func foo() {}
}

func bar(_ x: AnyObject) {
  // Message dispatch (crashing if the object doesn't respond to foo:).
  x.foo!()
}

Such calls always use message dispatch.

And I think that about summarises the current rules around which dispatch mechanisms are used where.


Once we have subclass MisunderstoodPerson and pass this instance to greetings(person:) this instance should be treated as Person. I have a confusion here and have couple of questions here.

  • The instance is of type MisunderstoodPerson so would witness table of MisunderstoodPerson be used here.
  • Or the instance has been typecast to Person, so would witness table of Person be used here.

(slight terminology nitpick: for classes, it's called a vtable rather than a witness table)

It's always the vtable that corresponds to the dynamic type of the instance that's used, so in this case it would be MisunderstoodPerson's vtable.

Snaky answered 5/4, 2019 at 10:46 Comment(7)
as per your statements is it safe to state protocol members declared in protocol extension use static dispatch. Also what do you mean by : The member is called on a protocol-typed value P or generic placeholder T : P. Are you saying that any instance of conforming type, typecasted to P ?Chiekochien
Do protocols also maintain vtables?Chiekochien
@RohanBhale By T : P, I mean if you have a function func foo<T : P>(_ x: T) { x.foo() }. If foo() is a protocol requirement of P, it will be dynamically dispatch through a protocol witness table. For a protocol-typed P value, yes I mean an instance of a conforming type casted (coerced, really) to P.Snaky
@RohanBhale Yes, a protocol witness table is generated for each conformance – https://mcmap.net/q/302457/-swift-protocol-extension-method-is-called-instead-of-method-implemented-in-subclass might be useful for more info on that.Snaky
Ok. Lets say I have MyClass conform to MyProtocol. I have an instance of MyClass named 'x'. You state 'It's always the vtable that corresponds to the dynamic type of the instance that's used.' Assume 'x' is typecast to MyProtocol reference is held in 'y' which is of type MyProtocol. Now If I call methods on 'y' can I safely say that even after typecasting to MyProtocol, MyClass vtable will be used and not MyProtocol's vtable.Chiekochien
@RohanBhale In cases where you're calling a dynamically dispatched class method through a protocol requirement, both tables are used. First the protocol witness table for MyClass is used to check whether it has provided an implementation of the requirement and then, assuming it has, the class instance's vtable is consulted (in case it has overriden the method).Snaky
Let us continue this discussion in chat.Chiekochien

© 2022 - 2024 — McMap. All rights reserved.