Draft proposal for adding Multimethods to C++

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 History
1.1 Changes from post-Oxford mailing version (28 Apr 2003) to pre-Kona mailing version (22 Sep 2003)
2 Multimethods as a C++ extension
2.1 Syntax
2.2 Dispatch algorithm
2.3 Dispatch speed
2.4 Miscellaneous issues
2.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 before main() is entered
3 Impact on the standard
4 Possible extensions
4.1 Friendship
4.2 Use of static types in the dispatch algorithm
4.3 Use with templated virtual parameters
5 Omitted features
5.1 Covarient returns
5.2 Multimethod member functions
5.3 Multimethod templates
5.4 Multimethod overloads
6 Other issues
6.1 Multimethod syntax, and getting direct access to multimethod implementations
6.1.1 Using a naming convention for multimethod implementations
6.1.2 User specification of multimethod implementation names
6.1.3 Special namespace for multimethod implementations
6.1.4 Use static_cast
6.1.5 Using pure-virtual-like syntax
6.2 Raw pointer to multimethod implementation function
6.3 Use with dynamic loading of code
6.4 Diagnostics
7 Multimethods resources
8 Acknowledgements
9 Cmm implementation issues
9.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

1 History

1.1 Changes from post-Oxford mailing version (28 Apr 2003) to pre-Kona mailing version (22 Sep 2003)

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.

2 Multimethods as a C++ extension

2.1 Syntax

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.

2.2 Dispatch algorithm

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:

  1. All of the best multimethod implementation's parameter types match the dynamic types.
  2. Each of the best multimethod implementation's parameter types is at least as derived as the corresponding parameter type in any other matching multimethod implementation.

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).

2.3 Dispatch speed

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.

2.4 Miscellaneous issues

2.4.1 Polymorphic classes

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.)

2.4.2 Allowed virtual parameter types

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&);

2.4.3 Multimethod exceptions

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.

2.4.4 Exception specifications

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.

2.4.5 Multimethod overloads

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.

2.4.6 Default parameters

Multimethod declarations can have default parameters, but multimethod implementations cannot have default parameters.

2.4.7 Use of multimethods before main() is entered

Multimethods 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.

3 Impact on the standard

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.

4 Possible extensions

4.1 Friendship

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.
};

4.2 Use of static types in the dispatch algorithm

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.

4.3 Use with templated virtual parameters

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:

  1. A function template 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.
  2. A function 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.
  3. A function 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).

5 Omitted features

Here are some features that have been suggested, with reasons why they are not included in the proposal.

5.1 Covarient returns

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.

5.2 Multimethod member functions

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.

5.3 Multimethod templates

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.

5.4 Multimethod overloads

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.

6 Other issues

6.1 Multimethod syntax, and getting direct access to multimethod implementations

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:

6.1.1 Using a naming convention for multimethod implementations

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.

6.1.2 User specification of multimethod implementation names

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.

6.1.3 Special namespace for multimethod implementations

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.

6.1.4 Use 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.

6.1.5 Using pure-virtual-like syntax

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.

6.2 Raw pointer to multimethod implementation function

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.

6.3 Use with dynamic loading of code

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.

6.4 Diagnostics

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?

7 Multimethods resources

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.

8 Acknowledgements

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.

9 Cmm implementation issues

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.

9.1 Generated code for multimethod dispatch

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.]

9.2 Registering multimethods using static initialisation

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.

9.3 Dispatch caches

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.

9.4 Dispatch functions

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.

9.5 Raw pointer to implementation function

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)) {...}
    }
}

9.6 Constant-time dispatch speed

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 ints 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.