Virtual functions are a great feature, but they have a rather severe limitation. They can only be implemented by extending the definition of all classes involved. A lot of operations on classes don’t need to be members, and sticking them in the class, just to use virtual dispatching, leads to interface pollution. I’m proposing here an out-of-class virtual dispatching syntax.
The basic problem
In my Leaf compiler code I have an object of type expression
, which could one of several sub-types. I wish to do some processing on this object. Some of the sub-types, not necessarily all, require special processing. Essentially I want to pass my object to one of these functions, depending on its type:
1 2 3 |
void process( expression ) void process( expression_binary ) void process( expression_function ) |
There are two basic approaches to this dispatch: visitors and switches.
A visitor pattern
If we’re willing to retrofit the types involved we can create a visitor pattern. Add a accept
function to each type in the class hierarchy and then build a visitor
object.
1 2 3 4 5 6 7 |
class process_visitor : expression_visitor { void visit( expression ) void visit( expression_binary ) void visit( expression_function ) } object.accept( process_visitor ) |
The problem here is that I haven’t avoided modifying the origin type. I need to add an accept
function to the base and every derived class. If I forget to add the function to a newly derived class then it breaks.
It also requires some kind of expression_visitor
interface to be defined. What exactly should that be? Usually it will contain a definition for all the subtypes of expression
. But I don’t always want to override the processing for all expressions.
Due to its limited functionality and a lot of boilerplate code I tend not to use the pattern often.
A big old switch
The other common approach is a switch
statement:
1 2 3 4 5 |
switch( type_of(object) ) { case t_binary: ... case t_function: ... default: ... } |
This requires some way to identify the types, either natively in the language, or with a custom type id. Additionally, the switch
statements I’ve seen don’t deal well with a value hierarchy. We can’t easily select a sub-tree of the type, instead we’re forced to itemize every leaf type. For this reason a series of if
statements is often used instead.
1 2 3 4 5 6 |
if( object instanceof expression_binary ) ) { ... } else if( object instanceof expression_function ) ) { ... } else { } |
This is a lot of repetitive coding, and it’s possible to get the order of the if statements wrong (like checking for a base type prior to a derived type).
Standalone virtual functions
Both approaches are just workarounds for what I really want: standalone virtual functions. I just want to express a series of functions with a common name that accept different types:
1 2 3 |
void process( virtual expression & ) = 0; void process( virtual expression_binary & b ); void process( virtual expression_funccall & f ); |
The virtual
bit expresses that dispatch to this overload is done based on the runtime type of the first parameter. I call this just as any other overloaded function:
1 |
process( object ); |
At run-time the dispatch will be done to the function best matching the type: following the overload rules as though it were a static dispatch with the most derived type.
This has several advantages over the other approaches:
- it is very compact without repetitive boilerplate code
- the original types are left unmodified
- it can dispatch over a subset of all the derived types
- it doesn’t require a user-defined type identifier
- it can deal with dispatching to mid-level types, not just leaf types
This feature would be a great addition to any OOP language with virtual functions. It’s a simple way to extend classes without touching the classes themselves.