Document number: WG21/N1529 = J16/03-0112
Author: Julian Smith (jules@op59.net)
Version: 2nd draft, $Date: 2003/09/22 00:34:28 $ $Revision: 1.23 $
1 History1.1 Changes from post-Oxford mailing version (28 Apr 2003) to pre-Kona mailing version (22 Sep 2003)2 Multimethods as a C++ extension2.1 Syntax3 Impact on the standard
2.2 Dispatch algorithm
2.3 Dispatch speed
2.4 Miscellaneous issues2.4.1 Polymorphic classes
2.4.2 Allowed virtual parameter types
2.4.3 Multimethod exceptions
2.4.4 Exception specifications
2.4.5 Multimethod overloads
2.4.6 Default parameters
2.4.7 Use of multimethods beforemain()
is entered
4 Possible extensions4.1 Friendship5 Omitted features
4.2 Use of static types in the dispatch algorithm
4.3 Use with templated virtual parameters5.1 Covarient returns6 Other issues
5.2 Multimethod member functions
5.3 Multimethod templates
5.4 Multimethod overloads6.1 Multimethod syntax, and getting direct access to multimethod implementations7 Multimethods resources6.1.1 Using a naming convention for multimethod implementations6.2 Raw pointer to multimethod implementation function
6.1.2 User specification of multimethod implementation names
6.1.3 Special namespace for multimethod implementations
6.1.4 Usestatic_cast
6.1.5 Using pure-virtual-like syntax
6.3 Use with dynamic loading of code
6.4 Diagnostics
8 Acknowledgements
9 Cmm implementation issues9.1 Generated code for multimethod dispatch
9.2 Registering multimethods using static initialisation
9.3 Dispatch caches
9.4 Dispatch functions
9.5 Raw pointer to implementation function
9.6 Constant-time dispatch speed
Altered: multimethod implementation syntax changed from trailing underscores to use of static
qualifiers. exception specification requirements fixed. removed ability to directly call multimethod implementations. covarient returns prohibited. exception class name changed. document number changed from 03-0046/N1463 to WG21/N1529 = J16/03-0111
New: auto-generated contents. need unambiguous down casts. virtual params can be pointers as well as references, with volatile as well as const qualifiers. multimethod templates discussion. default parameters prohibited in implementations. added discussions about possible extensions (friendship, static types in dispatch, templated virtual parameters such as smart pointers). prohibit overloaded multimethods. alternative syntaxes discussed.
Stroustrup's Design and Evolution of C++ suggests a syntax for multimethods in C++, which this proposal approximately follows.
A function prototype is a multimethod function if one or more of its
parameters are qualified with the keyword virtual
.
Implementations of the multimethod have these parameters qualified with a
static
.
The parameters that correspond to the virtual parameters in the virtual
function prototype must be derived from the original types, and have the same
modifiers (const
/volatile
). They must also derive
unambiguously from the original types - for example a cast from the base type
to the derived type must not give an `ambiguous base class' error.
Virtual function declarations shall occur before multimethod implementation function definitions/declarations. Multimethod implementation functions have the same name as the multimethod function but are not directly callable, e.g. they are renamed internally by the compiler.
Here is the classic multimethods example, an overlap()
function that takes references to a shape
base class. Detecting
whether two shapes overlap requires different code for each combination of
shapes:
struct shape {...};
struct square : shape {...};
struct triangle : shape {...};
bool overlap( virtual shape& a, virtual shape& b);
bool overlap( static square& a, static triangle& b) {...}
bool overlap( static triangle& a, static square& b) {...}
bool overlap( static shape& a, static square& b) {...}
bool overlap( static square& a, static shape& b) {...}
The overlap( virtual shape& a, virtual shape& b)
prototype is replaced by a dispatch function with prototype overlap(
shape& a, shape& b)
. Internally, this dispatch function uses
C++ RTTI to choose one of the available overlap()
functions,
based on the dynamic types of its parameters.
It is possible that there isn't any matching multimethod implementation function for a particular combination of dynamic types. In this case, the generated dispatch function will throw an exception. It will also throw an exception if there is no clear best multimethod implementation for a particular combination of dynamic types. See below for the new exception types that can be thrown.
A multimethod implementation is considered best only if both the following two conditions apply:
This is the same as the lookup rule used by C++ to resolve overloaded functions at compile time, apart from compile-time C++'s use of conversion operators and special-casing of built-in types.
Note that we cannot have duplicate multimethod implementations, so the second condition implies that for each other matching multimethod implementation X, the best multimethod implementation must have at least one parameter type that is more derived than the corresponding parameter type in X.
So if the available implementations of overlap
are as in the
above code example, then:
shape& s = *new square;
shape& t = *new triangle;
overlap( t, t); // Throws - no matching multimethod implementation.
overlap( s, t); // Calls overlap( static square&, static triangle&).
overlap( t, s); // Calls overlap( static triangle&, static square&).
overlap( s, s); // Throws - two multimethod implementations
// both match, but neither is a
// better match than the other:
// overlap( static shape&, static square&);
// overlap( static square&, static shape&);
There is also the possibility of ambiguities caused by a dynamic type multiply-inheriting from the same class more than once (a similar error can already occur at compile time if a static type multiply-inherits from the same class more than once).
The basic dispatch algorithm described above involves much use of RTTI and is linear in the number of multimethod implementations available, so it is very slow. But dispatch speed can be significantly improved using a cache.
For example. O(LogN) dispatch speed can be easily obtained using a
std::map
-style dispatch cache, mapping from arrays of
std::type_info
's to function pointers.
It is also possible to get constant-time dispatch if classes are assigned small unique integer identifiers, which could be stored in the vtable. This allows a dispatch array of pointers to functions to be created for each multimethod, using the small integer identifiers as indices (in effect, this is making vtables that belong to individual multimethod dispatch functions rather than individual classes).
To avoid whole-programme analysis at build-time, it may be best to initialise these small integers to zero, and assign non-zero values at runtime whenever a class is first involved in a multimethod call; this would also avoid wasting integer space on classes that aren't involved in multimethod calls, which in turn would reduce the sizes of the dispatch arrays.
A dispatch function for the overlap()
example using these
indices would look something like:
bool overlap( shape& a, shape& b)
{
typedef bool (*fnptr)( shape& a, shape& b);
static void fnptr** cache = NULL;
int a_index = a._vtable->small_integer;
int b_index = b._vtable->small_integer;
if ( a_index
&& b_index
&& cache
&& ((int) cache[0]) > a_index
&& cache[ a_index]
&& ((int) cache[ a_index][0]) > b_index
&& cache[ a_index][ b_index])
{
// fast dispatch:
return cache[ a_index][ b_index]( a, b);
}
else
{
/* Slow dispatch.
Ensure _vtable->small_integer's are assigned.
Ensure cache points to array of at least
a_index+1 items, (storing array size in
cache[0]).
Ensure cache[ a_index] points to array of at
least b_index+1 items, (storing array
size in cache[ a_index][0]).
Fill the cache using the slow algorithm
Make function call. */
...
}
}
The code leading to the `fast dispatch' could be implemented in very few assembler instructions (e.g. around 10 instructions on an ARM), resulting in multimethod dispatching that is comparable in speed to existing virtual function dispatch.
Classes that are used in multimethods shall be polymorphic - i.e. have at least one virtual function. (This is necessary to allow the generated dispatch code to use RTTI on the classes.)
Each virtual parameter must be one of:
If a virtual parameter is a pointer, passing a NULL pointer gives undefined behaviour.
Multimethod implementations must match the use of reference/pointer parameters in the original multimethod:
bool overlap( virtual shape&, virtual shape*);
...
bool overlap( static square& a, static triangle* b){...}
// Ok.
bool overlap( static circle& a, static ellipse& b) {...}
// Error - not an implementation of the multimethod,
// because second parameter should be a pointer.
One can use non-virtual parameters in a multimethod in addition to the virtual parameters. So a multimethod could look like:
int foo(
std::string&,
virtual Base&,
int x,
virtual const Base&);
There is a new abstract std::dispatch_error
class derived
from std::exception
. Derived from
std::dispatch_error
are two new classes,
std::dispatch_ambiguous
and
std::dispatch_unmatched
. How much information these classes
convey is implementation-defined.
If a multimethod has exception specifications, then implementations of the
multimethod must have exceptions specifications that are at least as
restrictive. However, multimethod implementations can ommit any
std::dispatch_error
from their multimethod's exception
specification if they do not call any multimethods themselves - dispatch
exceptions are generated before a multimethod's implementation is called.
Interaction of multimethod dispatch with these exception specifications is
much the same as for any other function. For example, if a multimethod has an
exception specification that does not include
std::dispatch_error
, and a multimethod call is ambiguous, then
std::unexpected()
will be called.
It is an error for a multimethod to overload a different multimethod with compatible virtual types.
So:
bool overlap( virtual shape&, virtual shape&, int);
bool overlap( virtual square&, virtual shape&, int);
// Error - square is derived from shape, and all other
// parameters are identical, so this overloads the
// previous multimethod.
bool overlap( virtual square&, virtual shape&, double);
// Ok - the third parameter makes things unambiguous.
Multimethod declarations can have default parameters, but multimethod implementations cannot have default parameters.
main()
is
enteredMultimethods shall not be called before main()
- i.e. static
initialisation code shall not call multimethods.
This restriction is purely to allow implementations that initialise
globals before main()
is entered, to use their own global
initialisation support to register multimethods at runtime, avoiding the need
to do whole-programme analysis at build time.
This proposal's syntax doesn't conflict with any existing C++ syntax.
Implementations that do not perform link-time analysis to collect all the
multimethod implementations in a programme, and do not initialise globals
before main()
is entered, will have to use a custom techique to
ensure that multimethod implementations are registered correctly before
main()
is entered.
Particular classes could nominate a multimethod as a friend; this would make all implementations of the multimethod friends of the class too.
Friendship could also be specified using a mix of static/virtual qualifiers to grant friendship to subsets of the available multimethod implementations:
void foo( virtual base1&, virtual base2&);
struct derived1 : base1
{
friend void bar( static derived1&, static base2&);
// Simple case: only this exact implementation is
// a friend.
friend void foo( static derived1&, virtual base2&);
// All multimethod implementations that take a
// `derived1&' as the first parameter are friends
// of derived1, regardless of the second parameter
// type.
// E.g. the following would be friends of derived1:
// void foo( static derived1& x, static base2&);
// void foo( static derived1& x, static derived2&);
friend void foo( virtual derived1&, virtual base2&);
// all implementations are friends of derived1.
};
We could allow the caller of a multimethod to mark some virtual parameters
with a static
prefix, telling the multimethod dispatcher to use
static types for these parameters, rather than their dynamic types.
If all virtual parameters are marked in this way, then the dispatch can in theory be resolved at build time. Note that this is not the same as conventional overloading, because the dispatch will still see matching implementations in all compilation units, not just the current compilation unit:
bool overlaps = overlap( a, b);
// Conventional dynamic dispatch using the dynamic types
// of both `a' and `b'.
bool overlaps = overlap( static a, b);
// Do multimethod dispatch but use the static type
// of `a' rather than its dynamic type, and the dynamic
// type of `b' as before.
bool overlaps = overlap( static a, static b);
// Do multimethod dispatch but use the static types
// of both `a' and `b' rather than their dynamic types.
// This call can be fully resolved at link-time.
bool overlaps = overlap(
static static_cast< triangle&>( a),
static static_cast< square&>( b));
// Example of using casts in conjunction with a request to
// use the static type. This avoids any runtime dispatch
// when we know at something about the dynamic types at
// compile time.
This is analogous to qualifying virtual function calls, where it is guaranteed that all matching virtual functions have seen (because all base classes must be visible when a derived class is visible). It is different from conventional compile-time overloading, which only considers what is visible in the current compilation unit.
Sometimes it is useful to pass a smart pointer to a function rather than a plain pointer or reference. For example the called function might want to copy the smart pointer into a persistent data structure before returning.
The implicit this
parameter passed to C++ member functions
doesn't allow this sort of usage without the user passing the smart pointer
explicitly:
struct foo
{
virtual void fn( shared_ptr< foo> ptr);
}
...
shared_ptr< foo> x = ...;
x->fn( x); // have to pass the smart pointer explicity
Multimethods could be extended to dispatch on the type of what a smart pointer parameter points to, rather than the type of the parameter itself:
void foo( shared_ptr< virtual base> x);
...
void foo( shared_ptr< static derived> x) {...}
void foo( shared_ptr< static otherderived> x) {...}
This requires three function templates to be provided that behave
similarly to dynamic_cast
, static_cast
and
typeid
, except that they know about particular class templates
and operate on what the class templates are handles for, rather than the
class templates themslves:
template_dynamic_test
that is similar
to dynamic_cast
, except that it returns
true
/false
depending on whether the dynamic
type of the pointee means that one could cast from one instantiation of
the class template to a different instantiation of the same class
template.template_static_cast
that is similar
to static_cast
, except that it casts from one instantiation
of the class template to a different instantiation of the same class
template. It assumes that a similar call to
template_dynamic_test
would return true
.template_typeid
that is similar to
typeid
except that it takes an instance of an instantiation
of a class template and returns the std::type_info
of the
object that it refers to.When static
is used inside the declaration of a function
parameter type template, it identifies the class that that parameter should
be treated as when performing dispatch. This means that one can write a whole
set of implementations that take a smart pointer to various derived types,
and the dispatch behaviour will be the same as if plain references had been
used instead of smart pointers.
For example, to make this scheme work with Boost's
boost::shared_ptr
, you would use the following definitions of
the three function templates:
#include "boost/smart_ptr.hpp"
template< class T>
const std::type_info&
template_typeid(
boost::shared_ptr< T> x)
{
return typeid( *x);
}
template< class derived, class base>
bool
template_dynamic_test(
boost::shared_ptr< base> x)
{
if ( boost::shared_dynamic_cast< derived>( x)).get()
return true;
return false;
}
template< class derived, class base>
boost::shared_ptr< derived>
template_static_cast(
boost::shared_ptr< base> x)
{
return boost::shared_static_cast< derived>( x);
}
These template specialisations have to be seen before each multimethod implementation.
Cmm 0.25 and later support templated virtual parameters (with slightly different names for the function templates).
Here are some features that have been suggested, with reasons why they are not included in the proposal.
By analogy with virtual functions, implementations of a multimethod that
returns a reference or pointer to a type T
, could be allowed to
return a reference or pointer to anything derived from T
.
However, this would only be useful if the multimethod implementations could
be called directly, which is not the case. The suggested extension to support
qualifying parameters at the point of call with the static
keyword does not change this - the function call is still in principle a call
to the dispatch function, not a specific multimethod implementation.
It has been suggested that it should be possible to make member functions into multimethods. I don't like this idea because it adds complications for no real gain in functionality. In particular, mixing virtual member functions and multimethods would be plain confusing and not do anything that couldn't be done by a straightforward multimethod.
In principle, it may be possible to have multimethod templates, such as:
template< class shapebase>
bool overlap( virtual shapebase&, virtual shapebase&);
// Dispatches to an overlap< shapebase>() implementation.
template< class shapebase>
bool overlap( static triangle< shapebase>& t) {...}
This would require extending export to instantiate any matching multimethod implementations in all source files whenever a particular multimethod template was instantiated.
In contrast, conventional class templates can have virtual functions without requiring separate compilation of templates, because one can put the virtual function definitions inside the class template itself, ensuring that it is instantiated whenever the class template is instantiated.
However, if export was extended to support these multimethod templates, then it would probably be straightforward to also have the equivalent of templated virtual member functions, of both ordinary classes and class templates, which are not supported in C++. This is because the restrictions imposed by vtable implementations of virtual functions don't really exist with multimethods. For example, multimethod dispatch tables are `owned' by the multimethod, not by a class.
In theory it would be possible to provide compile-time overloads of multimethods:
bool overlap( virtual shape&, virtual shape&);
bool overlap( virtual square&, virtual shape&);
This could be used to restrict the number of possible multimethod implementations that are available for consideration at runtime, so possibly improving dispatch speed where some type information is available at compile time. It would require that multimethod implementations are registered with more than one multimethod.
I suspect that this would be a little-used feature, which would add unnecessary complexity to the proposal, so it has been specifcally excluded.
Some may question the use of the static
keyword in
multimethod implementation function parameters. These keywords aren't
strictly required - the compiler knows whether a function definition is a
multimethod implementation by seeing whether it matches a previously
occurring multimethod declaration.
However, it is important to distinguish multimethod implementation functions from coventional functions, because they are not visible directly. Furthemore, the use of a special syntax enables the compiler to give a warning when the user mis-types the name of a multimethod implementation so that it doesn't match a multimethod declaration.
While static
is already used for many different concepts in
C++, it has a useful similarity with the phrase static type, and it
is not currently used inside function parameters declarations and so its
usage here doesn't cause confusion.
Some alternatives are:
Multimethod implementations could use a specific naming convention, such as having a trailing underscore:
bool overlap( virtual shape&, virtual shape&);
// A multimethod
bool overlap_( square& a, triangle& b) {...}
// An implementation of the above multimethod.
This is simple and easy to understand, but seems too arbitrary to be mandated in the C++ standard.
Instead of imposing a particular naming convention, we could let the user specify it:
bool overlap( virtual shape&, virtual shape&) = overlap_impl;
// A multimethod
bool overlap_impl( square& a, triangle& b) {...}
// An implementation of the above multimethod.
Another possibility would be to put multimethod implementations inside a special namespace:
bool overlap( virtual shape&, virtual shape&);
// Dispatches at runtime to one of the best-matching
// std_mm::overlap() functions.
namespace std_mm
{
bool overlap( square& a, triangle& b) {...}
...
}
bool overlaps = overlap( a, b); // calls multimethod
bool overlaps = std_mm::overlap( a, b);
// calls one of the visible implementations of overlap(),
// using conventional compile-time overloading.
static_cast
A different possibility is to use the existing function casting mechanism to select a particular multimethod implementation:
bool overlaps =
static_cast< bool (&)( square&, triangle&)>( overlap)
( shape1, shape2);
There are two problems with this. First, casting to the base-most parameters will give a base-most implementation (if it exists), which has the curious result that casting the multimethod to its own type can give a different function - the implementation that takes all base parameters:
bool overlap( virtual shape&, virtual shape&);
bool overlap( square&, square&) {...} // implementation 1.
bool overlap( shape&, shape&) {...} // implementation 2.
static_cast< bool (&)( square&, square&)>( overlap);
// Ok, gives implementation 1.
static_cast< bool (&)( shape, shape)>( overlap);
// Gives implementation 2, or could give the multimethod
// itself, because they both have the same signature.
static_cast< bool (&)( shape&, square&)>( overlap);
// Compile-time error - no exact match.
// But we might like it to give either implementation
// 2 or the multimethod, because they both match.
The second problem is that the specified types in the
static_cast
must match an implementation exactly. This is
contrary to the way qualified virtual fucntions work - calling
base::some_fn()
will look for some_fn
in class
base
or any of base
's base classes.
One way of avoiding the use of static
in multimethod
declarations and multimethod implementations, is to use virtual
for both, and use the pure-virtual syntax =0
for
multimethod.:
bool overlap( virtual base&, virtual base&) = 0;
// multimethod declaration.
bool overlap( virtual square& a, virtual triangle& b)
{...}
// multimethod implementation.
For speed critical loops, it might be useful to provide a way for the user to get a pointer to the best multimethod implementation function for a particular set of dynamic types. This enables client code that calls a multimethod on the same parameters many times in a loop, to cache the function pointer and so avoid any dispatch overhead.
For example, Cmm implements this as an extra dispatch function with the
same name as the multimethod, but with a suffix cmm_getimpl
.
If support for dynamic loading of code is added to the C++ standard in the future, then multimethod implementations in shared libraries/DLLs should be automatically registered/deregistered when shared libraries/DLLs are loaded/unloaded. The Cmm implementation already does this for Unix and Cygwin platforms.
If a multimethod implementation takes a virtual parameter that derives multiply from the base class used in the multimethod's corresponding virtual parmeter, then it will never be possible for that mutimethod to be called because down-casting from the base class to the derived class is ambiguous. Maybe this should thus cause a diagnostic at compile time?
Bjarne Stroustrup writes about multimethods in The Design and Evolution of C++ (Section 13.8, pages 297-301).
The Dylan language (see http://www.functionalobjects.com/resources/index.phtml) supports multimethods natively, as does CLOS (Common Lisp).
If one restricts oneself to Standard C++, it is possible to approximate multimethods at the expense of verbosity in source code. See Item 31 in Scott Meyers' More Effective C++, and chapter 11 of Andrei Alexandrescu's Modern C++ Design, where template techniques are used extensively. Bill Weston has a slightly different template technique that supports non-exact matches, see http://homepage.ntlworld.com/w.weston/.
Cmm, a C++ multimethods implementation in the form of a source-code translator, is available from http://www.op59.net/cmm/readme.html.
A paper about multimethods, C++ and Cmm is on the ACCU 2003 conference CD, also available online at http://www.op59.net/accu-2003-multimethods.html.
The Frost project (http://frost.flewid.de) adds support for multimethods to the i386-version of the gcc compiler.
Thanks to Bill Weston for many interesting and useful discussions about multimethods.
Thanks to Loise Goldthwaite, Kevlin Henney and Anthony Williams and other members of the UK C++ commitee for many comments about this proposal.
Cmm (See http://www.op59.net/cmm/readme.html) is a source-code processor which implements a multimethods language extension for C++. The description of Cmm's implementation here corresponds to cmm-0.26 (8 September 2003).
Cmm takes individual compilation units containing multimethod code that
have already been run through the C++ preprocessor (e.g.
#include
's have been expanded), and generates legal C++
compilation units, which can then be compiled and linked together
conventionally.
The generated C++ code calls some support functions that are provided as a
single source file called dispatch.cpp
. This contains functions
that manage data structures that store all known multimethod functions and
their implementations, the actual runtime dispatch functions, functions to
support dispatch caching and also support for the exception types thrown for
ambiguous/unmatched dispatches.
Cmm has been designed to generate multimethod code that supports dynamic loading and unloading of code, which means that all information about multimethods and their implementations are stored in dynamic data strucures. This is probably not directly relevent to this proposal, because the C++ standard doesn't concern itself with dynamic loading of code. However, it gives an example of the flexibility that the multimethods model can give.
Such implementation issues are not directly part of this proposal, but Cmm demonstrates that multimethods need not break the simple C compiler/linker model.
In the simple overlap multimethod example described earlier, The virtual function was:
bool overlap( virtual shape&, virtual shape&);
Consider an implementation of overlap
that is specialised for
a first parameter square
and second parameter
triangle
. This will look like:
// user-written multimethod implementation function
bool overlap_( square& a, triangle& b) {...}
In order to perform multimethod dispatch, one has to first decide which multimethod implementations match the dynamic types, and then try to find one of the multimethod implementations which can be considered a better match then all of the others.
The first step is done by calling auxiliary functions that Cmm creates for
each multimethod implementation function, which take the base parameters and
return true
only if each of the parameters can be
dynamic_cast
-ed to the multimethod implementation's parameters.
Because these functions takes the virtual function's base parameters, we
cannot use conventional overloading to distinguish them, and so Cmm makes the
function names unique using a mangling scheme which, for simplicity, will be
denoted by _XYZ
in the following:
// Cmm-generated match function for the function
// bool overlap( square& a, triangle& b);
bool overlap_cmm_match_XYZ( shape& a, shape& b)
{
if ( !dynamic_cast< square* >( &a)) return false;
if ( !dynamic_cast< triangle*>( &b)) return false;
return true;
}
This separate function is generated in the same compilation unit as the
multimethod implementation, which enables the dynamic_cast
to
work with derived types defined in anonymous namespaces. [Actually, the
overlap_cmm_match_XYZ
function takes an array of two
void*
's rather than a separate parameter for each virtual type,
each of which is static_cast
-ed to shape*
before
the dynamic_cast
is attempted. This is to enable generic
dispatch code to be used for different virtual functions.]
The second step requires that the inheritance tree for each dynamic type is known. The dispatch code can then compare the types taken by each matching multimethod implementation, and select the multimethod implementation for which each virtual parameter is no less derived than any other matching multimethod implementation's virtual parameter. As discussed earlier, this corresponds to the way conventional overloading works at compile time.
The information about the inheritance trees is encoded in C-style strings using a mangling scheme similar to that used by C++ compilers when generating symbol names. This allows static initialisation to be used to register multimethod implementations at runtime.
[Cmm can also register multimethod implementations at build time by requiring a separate link-style invocation, but this made builds very complicated and slow, and precludes use with dynamic loading of code. The only advantages of this scheme are that dispatch time may be slightly faster, and all multimethod implementations are usable by static initialisation code.]
Finally, the generic dispatch code calls the actual multimethod implementation via a wrapper function that takes the base parameters, casts them directly to the derived types, and calls the multimethod implementation. Again, this function name is mangled:
// Cmm-generated call function for the function
// bool overlap_( square& a, triangle& b);
bool overlap_cmm_call_XYZ( shape& a, shape& b)
{
return overlap(
*static_cast< square* >( &a),
*static_cast< triangle*>( &b));
}
This function's precondition is that the derived types are correct and so
the static_cast
's are legal. Using this wrapper function enables
the dispatch code to work in terms of generic function pointers even if
multimethod implementations use derived classes in anonymous namespace.
[The function should use dynamic_cast
rather than
static_cast
when derived
inherits virtually from
base
, but this hasn't been implemented yet.]
For each implementation, Cmm generates a global variable whose initialisation registers the implementation with the dispatch function:
static cmm_implementation_holder
overlap_XYZ(
"7overlap2_1_5shape1_5shape",
// the multimethod
"8overlap_2_2_5shape_6square2_5shape_8triangle",
// the multimethod implementation
overlap_cmm_implmatch_XYZ,
overlap_cmm_implcall_XYZ);
cmm_implementation_holder
is a class defined in
dispatch.h
/dispatch.cpp
, whose constructor
de-mangles the first two textual parameters to extract complete information
about the inheritance tree for each virtual parameter taken by the virtual
function and the implementation function. Together with the
overlap_cmm_match
functions, this is sufficient to enable
multimethod dispatch to be performed.
In this example, the first mangled string means: "A function called overlap with 2 virtual parameters, the first a class containing one item in its inheritance tree, shape, and the second also containing the same single class in its inheritance tree, shape". The second mangled string means: "A function called overlap_ with 2 virtual parameters, the first one being a class with 2 items in its inheritance tree, shape followed by square, while the second parameter's type also contains 2 items in its inheritance tree, the first one being shape, and the second triangle".
This use of static initialisers to register implementations allows
dynamically loaded code to automatically register new implementations with
the dispatch functions. Furthermore, the destructor of the
cmm_implementation_holder
class unregisters the implementations,
so one can load/unload code at will.
The handling of implementation functions in dynamically loaded code has
been succesfully tested on OpenBSD 3.2, OpenBSD 3.3, Slackware Linux 8.1 and
Cygwin/Windows, using the dlopen()
and dlclose()
functions.
Figuring out which of a set of implementation functions to call for a
particular set of dynamic types is very slow, so some sort of caching scheme
is required. Caching is performed by the code in the
dispatch.cpp
library. Currently this uses a
std::map
to map from a std::vector
of
std::type_info
's to a function pointer. This gives a dispatch
speed of O(Log N), where N is the number of different combinations of dynamic
types that have been encountered (some of which may be mapped to the same
function pointer). On OpenBSD 3.2 with gcc 2.95, the dispatch time for two
virtual parameters is around 10-100 times slower than a conventional virtual
function call.
It would probably be better to have special cache support for multimethods
with one or two virtual parameters, using a std::map
with key
types std::type_info[1]
and std::type_info[2]
. No
doubt templates could be used to do this with maximum obfuscation.
The actual virtual dispatch function is very simple, because it uses code
in dispatch.cpp
to do all the real work. This means that it is
practical to generate a separate copy in all compilation units as an inline
function, looking like:
inline bool overlap( shape& a, shape& b)
{
static cmm_virtualfn& virtualfn =
cmm_get_virtualfn( "7overlap2_1_5shape1_5shape");
typedef bool(*cmm_fntype)( shape&, shape&);
const void* params[] = { &a, &b};
const std::type_info* types[] =
{ &typeid( a), &typeid( b)};
cmm_fntype cmm_fn = reinterpret_cast< cmm_fntype>(
cmm_lookup( virtualfn, params, types));
return cmm_fn( a, b);
}
The cmm_lookup
function uses types
as an index
into the internal std::map
dispatch cache. If this fails, the
actual parameters params
are used in the slow lookup algorithm
described earlier. It returns a generic function pointer, which has to be
cast into the correct type using reinterpret_cast
.
Cmm provides an extra dispatch function that doesn't actually call the implementation. Instead, it returns a pointer to the best implementation function. This enables client code that calls a multimethod on the same parameters many times in a loop, to cache the function pointer and so avoid any dispatch overhead.
This extra dispatch function has the same name as the virtual function,
with a suffix _cmm_getimpl
. Using the overlap
example, if you have one collection of shapes that you know are all squares,
and you want to search for an overlap with a particular shape, you would
usually do:
void Fn( std::vector< square*> squares, shape& s)
{
std::vector< square*>::iterator it;
for( it=squares.begin(); it!=squares.end() ++ it)
{
if ( overlap( **it, s)) {...}
}
}
With the generated overlap_cmm_get_impl
function, you can
avoid the multimethod dispatch overhead in each iteration:
void Fn( std::vector< square*> squares, shape& s)
{
std::vector< square*>::iterator it;
if ( squares.empty()) return;
bool (*fn)( shape&, shape&) =
overlap_cmm_get_impl( s, squares[0]);
for( it=squares.begin(); it!=squares.end() ++ it)
{
if ( fn( **it, s)) {...}
}
}
As discussed earlier, it is possible to get constant-time dispatch speed if all types are assigned a unique small integer, by looking in a multi-dimensional array using the small integers as indices. Cmm has a command-line switch that makes the generated code use this technique.
In pseudo code, the dispatch for a function with two virtual parameters looks like:
void foo( virtual base& a, virtual base& b
int a_smallint = a.get_small_integer();
int b_smallint = b.get_small_integer();
fn = cache[a_smallint][b_smallint];
return fn( a, b);
In this case, cache
is essentially of type
fn_ptr**
.
To reduce memory usage, Cmm's dispatch.cpp
contains an
implementation of this dispatch method that allocates the array lazily so
that memory is only allocated for those rows that are actually used.
Getting a unique small integer for each dynamic type is slightly tricky. In an ideal world, the compiler and linker would conspire to make space available in the vtable, which would enable very fast lookup. Cmm can't do this though, so instead it inserts an inline virtual function body into all polymorphic classes, containing a static int to enable fast access to the unique integers:
class base
{
// Next function inserted by Cmm:
virtual int cmm_get_small_integer() const
{
static int id=0;
if ( !id) id =
cmm_create_small_integer( typeid( *this));
return id;
}
};
The function cmm_get_small_integer()
is in the Cmm library
dispatch.cpp
along with all of the other support function. It
maintains an internal map of std::type_info
's to
int
s so that it returns the same integer if called more than
once for the same type. This is required to make things work when the C++
environment doesn't implement the static int id
correctly; for
example, under OpenBSD 3.2 and 3.3, each compilation unit that contains the
inline function cmm_get_small_integer()
will have its own
copy.
Cmm's constant time dispatch system is not robust. It adds a virtual
function to all polymorphic classes, but this only works if all code is
passed through Cmm. Other code, such as code in libraries that are linked
into the final executable, may break at runtime because of assuming a
different vtable layout. To avoid breaking code in system libraries, Cmm
doesn't insert the function into polymorphic classes defined in the
std
namespace, but of course this means that you cannot do
constant-time multimethod dispatch on base classes that are defined in
std
, such as std::exception
.