[swift-evolution] Proposal: Universal dynamic dispatch for method calls

Kevin Ballard kevin at sb.org
Fri Dec 11 22:56:01 CST 2015


You think that Swift prefers virtual dispatch. I think it prefers static.

I think what's really going on here is that _in most cases_ there's no observable difference between static dispatch and virtual dispatch. If you think of Swift as an OOP language with a powerful value-typed system added on, then you'll probably think Swift prefers virtual dispatch. If you think of Swift as a value-typed language with an OOP layer added, then you'll probably think Swift prefers static dispatch. In reality, Swift is a hybrid language and it uses different types of dispatch in different situations as appropriate. In the OOP subset of the language, Swift is mostly virtual-dispatch, with a bit of dynamic-dispatch thrown in, and some opt-in static dispatch. In the value-typed subset of the language, Swift is static-dispatch.

Protocols are where it gets a little weird, as it's actually a combination of static and virtual dispatch, and runtime type information. When using generics, semantically speaking, the receiver type is statically resolved, and then it will use whatever dispatch the actual method in question uses (e.g. static for structs and values, virtual or dynamic for classes). Technically it does actually use virtual dispatch in the un-specialized case, but then specializes under optimization to match the described semantic behavior, but this is not an observable distinction. When using a protocol as a "protocol object" (i.e. a value typed as the protocol itself) it is of course always virtual dispatch (which may turn into dynamic dispatch for dynamic methods on classes). And the runtime type information part of protocols is the fact that you can always query the runtime type of the contained value, both checking what concrete type a protocol object has, and checking what protocols a value conforms to.

And finally for protocol extensions. These are strictly static typing, all the way. Which makes sense, because there's no virtual function table (or in Swift terms, protocol witness table) to put the methods into. Extensions can provide default implementations of protocol methods because the type that conforms to the protocol puts the extension method implementation into its own protocol witness table (and they only do this if the type doesn't already implement the method itself). Since the protocol witness table only contains things defined in the protocol itself, protocol extension methods that aren't part of the protocol don't get put anywhere. So invoking one of those methods has no possible virtual function table to check, the only thing it can do is statically invoke the method from the extension. And this is why you can't override them (or rather, why your override isn't called if the method resolution is done via the protocol).

The only way to make protocol extension methods work via virtual dispatch is to define a new protocol witness table for the extension itself, but types that were defined without being able to see the extension won't know to create and populate this protocol witness table. So now you need to be able to figure out if a protocol witness table exists at all before you can invoke it, and if it doesn't you have to fall back to static dispatch. Even worse, this means that even if the type already defines an appropriate override, because there's no protocol witness table the override can't get called. And that is a recipe for serious confusion, because you see that a method foo() is defined in an extension for protocol P, and you see that type T conforms to P and implements foo(), but calling foo() via the protocol will never invoke T.foo(). And there's no way for the user to know whether it will or will not work unless they know exactly which module P.foo() was defined in and exactly which module T.foo() was defined in and whether the module that defined T.foo() could see the module that defined P.foo() (and things get more complicated if the conformance of T: P was declared in yet a third module). This problem doesn't happen with protocols today because any type T that conforms to P by definition knows all the methods/properties that P defines and therefore can populate its protocol witness table.

The only other solution that comes to mind is turning all protocol dispatch into dynamic dispatch, which I hope you'll agree is not a good idea.

-Kevin Ballard

On Thu, Dec 10, 2015, at 10:35 PM, Brent Royal-Gordon wrote:
> >> Swift loves to dispatch things statically and does so wherever possible. In some cases—such as value types not having inheritance—language features are clearly designed the way they are specifically to allow them to be statically dispatched. But Swift never uses static dispatch where dynamic dispatch would have given a different result. This contrasts with, for instance, a non-virtual C++ method’s behavior of “ha ha ha, I’m just going to ignore your override for speed." You could write a Swift compiler which dispatched everything dynamically, and you would never see any difference in semantics.
> > 
> > Yes it does. When message dispatch is done using the obj-c runtime, the compiler is free to omit the message send and use static dispatch if it believes it knows the concrete type of the receiver. This normally behaves the same, because dynamic dispatch with a known receiver type and static dispatch are the same, except if the method is dynamically replaced using the obj-c runtime methods, the static dispatch will not invoke the dynamically overridden method. The most common way you see this is with KVO. If you try and KVO an @objc property on a Swift object, it may work sometimes and may not work at other times, depending on when the compiler believes it can safely use static dispatch. This is why Swift has a whole keyword called `dynamic` whose job is to say "no really, use dynamic dispatch for every single access to this method/property, I don't care that you know for a fact it's a Foo and not a subclass, just trust me on this”.
> 
> Okay, time to introduce some precise definitions so we don’t talk past each other.
> 
> There are three types of dispatch:
> 
> * Static. The compiler determines the exact function to execute.
> * Virtual (I was previously calling this “dynamic”). The compiler determines the function to execute’s position in the instance’s vtable. (I don’t know if Swift actually calls this data structure a “vtable”, but you get my meaning.)
> * Dynamic. The compiler determines the selector to send to the instance, thereby causing a function to execute.
> 
> Swift prefers virtual dispatch. Unless you request dynamic dispatch with `dynamic`, or you’re using members written in Objective-C (which aren’t included in the vtables Swift uses for virtual dispatch), Swift always behaves as if you’re going to get at least virtual dispatch. In some cases it uses static dispatch, but only where the language’s semantics guarantee that virtual dispatch would give the same result. Examples of this include `final` and `static` members (which forbid overriding the members in question, and thus prevent a mismatch between the results of virtual dispatch and static) and value types (which don’t support inheritance, so there’s no way to introduce a mismatch).
> 
> Again, the sole exception to this is protocol extensions. Unlike any other construct in the language, protocol extension methods are dispatched statically in a situation where a virtual dispatch would cause different results. No compiler error prevents this mismatch.
> 
> > More generally, I don't see there being any real difference between
> > 
> > extension Proto {
> >    func someMethodNotDefinedInTheProto()
> > }
> > 
> > and saying
> > 
> > func someFuncThatUsesTheProto<T: Proto>(x: T)
> > 
> > except that one is invoked using method syntax and one is invoked using function syntax. Method invocation syntax does not inherently mean "dynamic dispatch" any more than function syntax does.
> 
> I’m sorry, I just don’t agree with you on this. In Swift, generics and overloads are statically resolved at compile time. Other than explicit branching constructs like `if` and `switch`, the *only* place in Swift where a dynamic, *runtime* type—as opposed to the static, *compile-time* type—chooses which code to run is in the self position of a member access. Swift may choose to use static dispatch in certain cases, but the language is designed to prevent any difference in semantics based on that decision.
> 
> Protocol extensions are the weird outlier here—the only case in which the static and virtual dispatch behaviors are different, and Swift chooses the static dispatch behavior.
> 
> -- 
> Brent Royal-Gordon
> Architechies
> 


More information about the swift-evolution mailing list