Double dispatch
In software engineering, double dispatch is a special form of multiple dispatch, and a mechanism that dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call. In most object-oriented systems, the concrete function that is called from a function call in the code depends on the dynamic type of a single object and therefore they are known as single dispatch calls, or simply virtual function calls.
Dan Ingalls first described how to use double dispatching in Smalltalk, calling it multiple polymorphism.
Overview
The general problem addressed is how to dispatch a message to different methods depending not only on the receiver but also on the arguments.To that end, systems like CLOS implement multiple dispatch. Double dispatch is another solution that gradually reduces the polymorphism on systems that do not support multiple dispatch.
Use cases
Double dispatch is useful in situations where the choice of computation depends on the runtime types of its arguments. For example, a programmer could use double dispatch in the following situations:- Sorting a mixed set of objects: algorithms require that a list of objects be sorted into some canonical order. Deciding if one element comes before another element requires knowledge of both types and possibly some subset of the fields.
- Adaptive collision algorithms usually require that collisions between different objects be handled in different ways. A typical example is in a game environment where the collision between a spaceship and an asteroid is computed differently from the collision between a spaceship and a spacestation.
- Painting algorithms that require the intersection points of overlapping sprites to be rendered in a different manner.
- Personnel management systems may dispatch different types of jobs to different personnel. A
schedule
algorithm that is given a person object typed as an accountant and a job object typed as engineering rejects the scheduling of that person for that job. - Event handling systems that use both the event type and the type of the receptor object in order to call the correct event handling routine.
- Lock and key systems where there are many types of locks and many types of keys and every type of key opens multiple types of locks. Not only do you need to know the types of the objects involved, but the subset of "information about a particular key that are relevant to seeing if a particular key opens a particular lock" is different between different lock types.
A common idiom
Example in Ruby
A common use case is displaying an object on a display port which can be a screen or a printer, or something else entirely that doesn't exist yet. This is a naive implementation of how to deal with those different media.class Rectangle
def display_on
# selects the right code based on the object class
case port
when DisplayPort
# code for displaying on DisplayPort
when PrinterPort
# code for displaying on PrinterPort
when RemotePort
# code for displaying on RemotePort
end
end
end
The same would need to be written for Oval, Triangle and any other object that wants to display itself on a medium. The problem is that more than one degree of polymorphism exist: one for dispatching the display_on method to an object and another for selecting the right code for displaying.
A much cleaner and more maintainable solution is then to do a second dispatch, this time for selecting the right method for displaying the object on the medium:
class Rectangle
def display_on
# second dispatch
port.display_rectangle
end
end
class Oval
def display_on
# second dispatch
port.display_oval
end
end
class DisplayPort
def display_rectangle
# code for displaying a rectangle on a DisplayPort
end
def display_oval
# code for displaying an oval on a DisplayPort
end
#...
end
class PrinterPort
def display_rectangle
# code for displaying a rectangle on a PrinterPort
end
def display_oval
# code for displaying an oval on a PrinterPort
end
#...
end
Double dispatch in C++
At first glance, double dispatch appears to be a natural result of function overloading. Function overloading allows the function called to depend on the type of the argument. Function overloading, however, is done at compile time using "name mangling" where the internal name of the function encodes the argument's type. For example, a functionfoo
may internally be called __foo_i and the function foo
may be called __foo_d. Thus, there is no name collision, and no virtual table lookup. By contrast, dynamic dispatch is based on the type of the calling object, meaning it uses virtual functions instead of function overloading, and does result in a vtable lookup. Consider the following example, written in C++, of collisions in a game:class SpaceShip ;
class ApolloSpacecraft : public SpaceShip ;
class Asteroid ;
class ExplodingAsteroid : public Asteroid ;
If you have:
Asteroid theAsteroid;
SpaceShip theSpaceShip;
ApolloSpacecraft theApolloSpacecraft;
then, because of function overloading,
theAsteroid.CollideWith;
theAsteroid.CollideWith;
will print, respectively,
Asteroid hit a SpaceShip
and Asteroid hit an ApolloSpacecraft
, without using any dynamic dispatch. Furthermore:ExplodingAsteroid theExplodingAsteroid;
theExplodingAsteroid.CollideWith;
theExplodingAsteroid.CollideWith;
will print
ExplodingAsteroid hit a SpaceShip
and ExplodingAsteroid hit an ApolloSpacecraft
respectively, again without dynamic dispatch.With a reference to an
Asteroid
, dynamic dispatch is used, and this code:Asteroid& theAsteroidReference = theExplodingAsteroid;
theAsteroidReference.CollideWith;
theAsteroidReference.CollideWith;
prints
ExplodingAsteroid hit a SpaceShip
and ExplodingAsteroid hit an ApolloSpacecraft
, again as expected. However, the following code does not work as desired:SpaceShip& theSpaceShipReference = theApolloSpacecraft;
theAsteroid.CollideWith;
theAsteroidReference.CollideWith;
The desired behaviour is to bind these calls to the function that takes
theApolloSpacecraft
as its argument, as that is the instantiated type of the variable, meaning the expected output would be Asteroid hit an ApolloSpacecraft
and ExplodingAsteroid hit an ApolloSpacecraft
. However, the output is actually Asteroid hit a SpaceShip
and ExplodingAsteroid hit a SpaceShip
. The problem is that, while virtual functions are dispatched dynamically in C++, function overloading is done statically.The problem described above can be resolved by simulating double dispatch, for example by using a visitor pattern. Suppose the existing code is extended so that both
SpaceShip
and ApolloSpacecraft
are given the functionvirtual void CollideWith
Then, while the previous example still does not work correctly, reframing the calls so that the spaceship is the agent gives us the desired behaviour:
SpaceShip& theSpaceShipReference = theApolloSpacecraft;
Asteroid& theAsteroidReference = theExplodingAsteroid;
theSpaceShipReference.CollideWith;
theSpaceShipReference.CollideWith;
It prints out
Asteroid hit an ApolloSpacecraft
and ExplodingAsteroid hit an ApolloSpacecraft
, as expected. The key is that theSpaceShipReference.CollideWith;
does the following at run time:-
theSpaceShipReference
is a reference, so C++ looks up the correct method in the vtable. In this case, it will callApolloSpacecraft::CollideWith
. - Within
ApolloSpacecraft::CollideWith
,inAsteroid
is a reference, soinAsteroid.CollideWith
will result in another vtable lookup. In this case,inAsteroid
is a reference to anExplodingAsteroid
soExplodingAsteroid::CollideWith
will be called.Double dispatch in C#
Double dispatch in Eiffel
The Eiffel programming language can bring the concept of agents to bear on the double-dispatch problem. The example below applies the agent language construct to the double-dispatch problem.Consider a problem domain with various forms of SHAPE and of drawing SURFACE upon which to draw a SHAPE. Both SHAPE and SURFACE know about a function called `draw' in themselves, but not in each other. We want objects of the two types to co-variantly interact with each other in a double-dispatch using a visitor pattern.
The challenge is to get a polymorphic SURFACE to draw a polymorphic SHAPE on itself.
Output
The output example below shows the results of two SURFACE visitor objects being polymorphically passed over a list of polymorphic SHAPE objects. The visitor code pattern is only aware of SHAPE and SURFACE generically and not of the specific type of either. Instead, the code relies on run-time polymorphism and the mechanics of agents to achieve a highly flexible co-variant relationship between these two deferred classes and their descendants.draw a POLYGON on ETCHASKETCH
draw a POLYGON on GRAFFITI_WALL
draw a RECTANGLE on ETCHASKETCH
draw a RECTANGLE on GRAFFITI_WALL
draw a QUADRILATERAL on ETCHASKETCH
draw a QUADRILATERAL on GRAFFITI_WALL
draw a PARALLELOGRAM on ETCHASKETCH
draw a PARALLELOGRAM on GRAFFITI_WALL
draw a POLYGON on ETCHASKETCH
draw a POLYGON on GRAFFITI_WALL
draw a RECTANGLE on ETCHASKETCH
draw a RECTANGLE on GRAFFITI_WALL
Setup
Before looking at SHAPE or SURFACE, we need to examine the high level decoupled use of our double-dispatch.Visitor pattern
The visitor pattern works by way of a visitor object visiting the elements of a data structure polymorphically, applying some action against the polymorphic element objects in the visited target structure.In our example below, we make a list of polymorphic SHAPE objects, visiting each of them with a polymorphic SURFACE, asking the SHAPE to be drawn on the SURFACE.
make
-- Print shapes on surfaces.
local
l_shapes: ARRAYED_LIST
l_surfaces: ARRAYED_LIST
do
create l_shapes.make
l_shapes.extend
l_shapes.extend
l_shapes.extend
l_shapes.extend
l_shapes.extend
l_shapes.extend
create l_surfaces.make
l_surfaces.extend
l_surfaces.extend
across l_shapes as ic_shapes loop
across l_surfaces as ic_surfaces loop
ic_surfaces.item.drawing_agent
end
end
end
We start by creating a collection of SHAPE and SURFACE objects. We then iterate over one of the lists, allowing elements of the other to visit each of them in turn. In the example code above, SURFACE objects are visiting SHAPE objects.
The code makes a polymorphic call on.draw indirectly by way of the `drawing_agent', which is the first call of the double-dispatch pattern. It passes an indirect and polymorphic agent, allowing our visitor code to only know about two things:
- What is the drawing agent of the surface ?
- What is the drawing data agent of the shape ?
SURFACE
Within the polymorphic call to.draw is the call to an agent, which becomes the second polymorphic call or dispatch in the double-dispatch pattern.deferred class
SURFACE
feature -- Initialization
make
-- Initialize Current.
do
drawing_agent := agent draw
end
feature -- Access
drawing_agent: PROCEDURE
do
l_result := a_data_agent
end
type: STRING
-- Type name of Current.
deferred end
end
The agent argument in line #19 and call in line #24 are both polymorphic and decoupled. The agent is decoupled because the.draw feature has no idea what class `a_data_agent' is based on. There is no way to tell what class the operation agent was derived from, so it does not have to come from SHAPE or one of its descendants. This is a distinct advantage of Eiffel agents over the single inheritance, dynamic and polymorphic binding of other languages.
The agent is dynamically polymorphic at run-time because the object is created in the moment it is needed, dynamically, where the version of the objectified routine is determined at that time. The only strongly bound knowledge is of the Result type of the agent signature—that is—a named TUPLE with two elements. However, this specific requirement is based on a demand of the enclosing feature, which is necessary and has not been avoided.
Finally, note how only the `drawing_agent' feature is exported to ANY client! This means that the visitor pattern code only needs to know about the agent to get its job done.
SHAPE
The SHAPE class has the basis for what is drawn, perhaps on a SURFACE, but it does not have to be. Again, the agents provide the indirection and class agnostics required to make the co-variant relationship with SHAPE as decoupled as possible.Additionally, please take note of the fact that SHAPE only provides `drawing_data_agent' as a fully exported feature to any client. Therefore, the only way to interact with SHAPE, other than creation, is through the facilities of the `drawing_data_agent', which is used by ANY client to indirectly and polymorphically gather drawing data for the SHAPE!
deferred class
SHAPE
feature -- Initialization
make_with_color
-- Make with `a_color' as `color'.
do
color := a_color
drawing_data_agent := agent drawing_data
ensure
color_set: color.same_string
end
feature -- Access
drawing_data_agent: FUNCTION
-- Data agent for drawing.
feature -- Implementation
drawing_data: TUPLE
-- Data needed for drawing of Current.
do
Result :=
end
name: STRING
-- Object name of Current.
deferred end
color: STRING
-- Color of Current.
end
Classic Spaceship example
A variation of the classic Spaceship example has one or more spaceship objects roaming about a universe filled with other items like rogue asteroids and space stations. What we want is a double-dispatch method for handling encounters between two co-variant objects in our make-believe universe.In our example below, the output excursion of our USS Enterprise and USS Excelsior will be:
Starship Enterprise changes position from A-001 to A-002.
Starship Enterprise takes evasive action, avoiding Asteroid `Rogue 1'!
Starship Enterprise changes position from A-002 to A-003.
Starship Enterprise takes evasive action, avoiding Asteroid `Rogue 2'!
Starship Enterprise beams a science team to Starship Excelsior as they pass!
Starship Enterprise changes position from A-003 to A-004.
Starship Excelsior changes position from A-003 to A-005.
Starship Enterprise takes evasive action, avoiding Asteroid `Rogue 3'!
Starship Excelsior is near Space Station Deep Space 9 and is dockable.
Starship Enterprise changes position from A-004 to A-005.
Starship Enterprise beams a science team to Starship Excelsior as they pass!
Starship Enterprise is near Space Station Deep Space 9 and is dockable.
Visitor
The visitor for the classic Spaceship example also has a double-dispatch mechanism.make
-- Allow SPACESHIP objects to visit and move about in a universe.
local
l_universe: ARRAYED_LIST
l_enterprise,
l_excelsior: SPACESHIP
do
create l_enterprise.make_with_name
create l_excelsior.make_with_name
create l_universe.make
l_universe.force
l_universe.force
l_universe.force
l_universe.force
l_universe.force
l_universe.force
visit
l_enterprise.set_position
visit
l_enterprise.set_position
visit
l_enterprise.set_position
l_excelsior.set_position
visit
visit
l_enterprise.set_position
visit
end
feature -- Implementation
visit
-- `a_object' visits `a_universe'.
do
across a_universe as ic_universe loop
check attached ic_universe.item as al_universe_object then
a_object.encounter_agent.call
end
end
end
The double-dispatch can be see in line #35, where two indirect agents are working together to provide two co-variant calls working in perfect polymorphic concert with each other. The `a_object' of the `visit' feature has an `encounter_agent' which is called with the sensor data of the `sensor_data_agent' coming from the `al_universe_object'.
The other interesting part of this particular example is the SPACE_OBJECT class and its `encounter' feature:
Visitor action
The only exported features of a SPACE_OBJECT are the agents for encounter and sensor data, as well as the capacity to set a new position. As one object visits each object in the universe, the sensor data is collected and passed to the visiting object in its encounter agent. There, the sensor data from the sensor_data_agent are evaluated against the current object and a course of action is taken based on that evaluation.All other data is exported to. This is similar to C, C++ and Java scopes of Private. As non-exported features, the data and routines are used only internally by each SPACE_OBJECT.
Finally, note that encounter calls to `print' do not include specific information about possible descendant classes of SPACE_OBJECT! The only thing found at this level in the inheritance are general relational aspects based completely on what can be known from the attributes and routines of a general SPACE_OBJECT. The fact that the output of the `print' makes sense to us, as human beings, based on what we know or imagine about Star ships, space stations and asteroids is merely logical planning or coincidence. The SPACE_OBJECT is not programmed with any specific knowledge of it descendants.
deferred class
SPACE_OBJECT
feature -- Initialization
make_with_name
-- Initialize Current with `a_name' and `a_position'.
do
name := a_name
position := a_position
sensor_data_agent := agent sensor_data
encounter_agent := agent encounter
ensure
name_set: name.same_string
position_set: position.same_string
end
feature -- Access
encounter_agent: PROCEDURE
-- Agent for managing encounters with Current.
sensor_data_agent: FUNCTION
-- Agent for returning sensor data of Current.
feature -- Settings
set_position
-- Set `position' with `a_position'.
do
position := a_position
ensure
position_set: position.same_string
end
feature -- Implementation
encounter
-- Detect and report on collision status of Current with `a_radar_agent'.
do
a_sensor_agent.call
check attached a_sensor_agent.last_result as al_sensor_data then
if not name.same_string then
if then
if and
and
) then
elseif and
and
) then
elseif then
end
end
end
end
end
name: STRING
-- Name of Current.
type: STRING
-- Type of Current.
deferred
end
position: STRING
-- Position of Current.
is_dockable: BOOLEAN
-- Is Current dockable with another manned object?
deferred
end
is_manned: BOOLEAN
-- Is Current a manned object?
deferred
end
is_manueverable: BOOLEAN
-- Is Current capable of being moved?
deferred
end
sensor_data: attached like sensor_data_anchor
-- Sensor data of Current.
do
Result :=
end
sensor_data_anchor: detachable TUPLE
-- Sensor data type anchor of Current.
end
There are three descendant classes of SPACE_OBJECT:
SPACE_OBJECT
ASTEROID
SPACESHIP
SPACESTATION
In our example, the ASTEROID class is used for the `Rogue' items, SPACESHIP for the two star ships and SPACESTATION for Deep Space Nine. In each class, the only specialization is the setting of the `type' feature and of certain properties of the object. The `name' is supplied in the creation routine as well as the `position'.
For example: Below is the SPACESHIP example.
class
SPACESHIP
inherit
SPACE_OBJECT
create
make_with_name
feature -- Implementation
type: STRING = "Starship"
--
is_dockable: BOOLEAN = True
--
is_manned: BOOLEAN = True
--
is_manueverable: BOOLEAN = True
--
end
So, any SPACESHIP in our universe is dock-able, manned and maneuverable. Other objects, like Asteroids are none of these things. A SPACESTATION, on the other hand, is both dock-able and manned, but is not maneuverable. Thus, when one object has an encounter with another, it first checks to see if the positions put them in the vicinity of each other and if they are, then the objects interact based upon their basic properties.
Note that objects with the same type and name are considered to the same object, so an interaction is logically disallowed.
Eiffel example conclusion
With regards to double-dispatch, Eiffel allows the designer and programmer to further remove a level of direct object-to-object knowledge by decoupling class routines from their classes by way of making them agents and then passing those agents instead of making direct object feature calls. The agents also have specific signatures and possible results, making them ideal static type checking vehicles without giving up specific object details. The agents are fully polymorphic so that the resulting code has only the specific knowledge required to get its local job done. Otherwise, there is no maintenance burden added by having specific internal class feature knowledge spread around many co-variant objects. The use and mechanics of agents ensure this.One possible downside of the use of agents is that an agent is computationally more expensive than its direct call counterpart. With this in mind, one ought never to presume the use of agents in the double-dispatch and their application in visitor patterns. If one can clearly see a design limit as to the domain of class types that will be involved in the co-variant interactions, then a direct call is the more efficient solution in terms of computational expense. However, if the class domain of participating types is expected to grow or differ substantially, then agents present an excellent solution to lessening the maintenance burden in the double-dispatch pattern.