Dynamic vs. Static Dispatch
This article explains the difference between dynamic dispatch (late binding) and static dispatch (early binding). We'll also touch on the differences in language support for virtual and static methods, and how virtual methods can be circumvented.
Dynamic dispatch is a defining feature of object-oriented programming. So what is so special about it?
Static dispatch (or early binding) happens when I know at compile time which function body will be executed when I call a method. In contrast, dynamic dispatch (or run-time dispatch or virtual method call or late binding) happens when I defer that decision to run time. This runtime dispatch requires either an indirect call through a function pointer, or a name-based method lookup.
Run-time dispatch adds a lot of flexibility to our system. For example, we can combine components at run time, or even swap out components in a running system. This is used for dynamic loading of DLL files (Windows) or SO files (Unix, Linux). Within OOP systems, this run-time configuration is often called dependency injection.
But dynamic dispatch allows much more. In object-oriented languages, a subclass can change the behaviour of a base class by overriding some methods. The ability of a base class to call a method that is resolved through dynamic dispatch to a subclass method is called open recursion. Open recursion requires that any calls to an overridable method must use dynamic dispatch, even when the target seems obvious – this is illustrated in a code snippet below.
Virtual and static method dispatch in various languages.
All OOP languages necessarily support virtual methods in some form. Some languages support only virtual methods.
One such language is Java.
In Java, all instance methods are virtual by default.
Whether this is a good or a bad thing is debatable: it makes the class more flexible since anything can be overridden in a subclass, but for the same reason also more fragile.
Virtual dispatch is unnecessary when the method can't be overridden, so there are two specific cases where static dispatch may be used safely:
when overriding the method is prevented by marking it as final
,
or when the method is private
as the method can only be called from within this class.
Java's “static methods” do use static dispatch, but they can't be used as instance methods so they aren't relevant here.
Another language without static method dispatch is Python.
In fact, Python uses late binding for everything, even normal function calls and variables.
Python has a thing called “static methods”, but those are completely unrelated to early binding.
Their defining feature is that a method decorated with @staticmethod
may be called on either a class or an instance.
Consequently, it has neither a self
or cls
argument as instance methods or class methods would do.
Other languages give us a choice and explicitly support both virtual and non-virtual methods, such as C++ and C#. Here, methods are non-virtual by default. They may still be overridden/shadowed in subclasses, but the non-virtual method is resolved purely according to the static type of the object at the call site. Such non-virtual methods do not have open recursion; a C++ example demonstrating this difference follows below.
In C++, it is tremendously important that virtual methods aren't the default option, because:
-
Virtual dispatch is only needed under certain circumstances – if you want polymorphism. The extra indirection required for virtual methods makes virtual method calls a bit less efficient, though that's typically unnoticeable. More importantly, virtual methods make a class more complex since the behaviour of a class with virtual methods also depends on all subclasses. That is absolutely not desirable when writing robust software.
-
Virtual methods affect the object layout. An object with virtual methods needs an additional hidden member: a pointer to a vtable. A vtable is a lookup table mapping virtual methods to an implementation, effectively like an array of function pointers. A mandatory vtable pointer (as in Java) makes zero-cost abstractions impossible, and would make struct layout incompatible with C.
With the motto that you “don't pay for what you don't use”, C++ requires explicit acknowledgement from the programmer that they want to pay the price of virtual methods.
Forcing static dispatch.
Some languages allow us to use a special method call syntax to force static dispatch. This is useful when we want to avoid open recursion, e.g. to make our base class more robust against changes in subclasses. Avoiding virtual dispatch may also be necessary in certain multiple inheritance scenarios, when we want to dispatch to a specific base.
Some languages allow function call syntax for methods. In such a case, the programmer selects the fully qualified name of the method, so no dynamic dispatch is necessary.
In Python: Class.method(object, a, b)
.
Note that this still uses late binding to resolve Class.method
, but no dynamic dispatch happens when that method is applied.
In Perl: Class::method($object, $a, $b)
.
This really uses static dispatch if the Class::method
was already defined at the point of this call.
In such a case we can also omit the parenthesis, since the function can be used as an operator: Class::method $object, $a, $b
.
In Perl, the only thing that turns a function into a method is that it's invoked as a method ($object->method($a, $b)
).
In JavaScript: Class.prototype.method.call(object, a, b)
.
This is pretty much the same as in Python, except that we now have an extra level to retrieve the prototype, and need to use call
to invoke the method.
By invoking a function with call
instead of invoking it directly, the first argument is bound to this
.
Some languages allow fully qualified method names in their method call syntax.
In C++: object.Class::method(a, b)
.
The compiler makes sure that the Class
is a base of the object
, so calls outside of the inheritance hierarchy are not possible.
In Perl: $object->Class::method($a, $b)
.
Technically this is still implemented in terms of late binding, but subroutine lookup is used instead of method lookup.
This is subtly different from Class::method($object, $a, $b)
.
With the function call syntax, the $object
may be any value, but with the method call syntax, the $object
must either be an object or a string that look like a class name (Perl is a bit weird sometimes).
Unlike C++, there are no restrictions on the invoked method.
As a language design observation, note that this syntax is possible only because the method call operator (.
or ->
) is different from the namespace operator ::
.
Java had to introduce a method reference operator ::
since .
was already overloaded thrice with namespace traversal and method invocation and field access.
Speaking of Java:
there's no way to statically dispatch to an instance method.
I originally thought it might be possible to call a method reference
like Class::method
by wrapping it in a functional interface
like ((Function3<Class, A, B, R>) Class::method).apply(object, a, b)
where @FunctionalInterface interface Function3<A1, A2, A3, R> { R apply(A1 a1, A2 a2, A3 a3); }
.
This is not only terribly indirect and confusing, this also doesn't actually work.
The method reference doesn't refer to the concrete method implementation method
as statically visible in Class
, but to the corresponding method slot.
If we invoke that method reference on an object which has overridden this method, the overridden method will be called instead.
Illustrating Dynamic Dispatch and Open Recursion with the Template Method Pattern.
All of the OOP design patterns rely on virtual methods.
A simple example is the template method pattern.
Here, a base class defines a method that may be overridden in a subclass to modify the behaviour of a base class.
In contrast to the virtual-by-default Java methods, this is an intentional extension point, often with the protected
access modifier.
Here is an example program in C++ using the template method pattern.
By defining or undefining the preprocessor macro USE_VIRTUAL_DISPATCH
, we can turn virtual dispatch on or off and observe the difference.
#include <iostream>
// use macros to switch between virtual and static dispatch
#ifdef USE_VIRTUAL_DISPATCH
#define METHOD virtual
#define OVERRIDE override
#else
#define METHOD /*non-virtual*/
#define OVERRIDE /*override*/
#endif
class Base {
public:
// invoke the templateMethod from the static context of the base class
void consumeTemplate() const {
std::cout << templateMethod() << std::endl;
}
protected:
// may be overridden in subclasses
METHOD std::string templateMethod() const {
return "base class";
}
};
class Subclass : public Base {
protected:
// modify base class behaviour
METHOD std::string templateMethod() const OVERRIDE {
return "subclass";
}
};
int main() {
Subclass().consumeTemplate();
return 0;
}
If we compile this with -UUSE_VIRTUAL_DISPATCH
(-U
undefines that macro), then this will print base class
.
Inside Base::consumeTemplate()
, the templateMethod()
call is known to refer to Base::templateMethod()
.
The Base
does not know about the Subclass::templateMethod()
.
But if we recompile with -DUSE_VIRTUAL_DISPATCH
(-D
defines that macro),
then this will print subclass
.
The macro causes the templateMethod()
to be defined as virtual
.
Now, the Base::consumeTemplate()
method only knows that this
object supports some templateMethod()
operation, but not which class provides this operation.
It somehow needs to search the object for the correct method.
Since the used object is actually a Subclass
instance, it will use Subclass::templateMethod()
.
Only if the language has a virtual dispatch mechanism that behaves in this way does it have open recursion.
- next post: Should I Separate Unit Tests from Integration Tests?
- previous post: Simpler Tests thanks to “Extract Method” Refactoring