<<

Open Multi-Methods for ++

Peter Pirkelbauer Yuriy Solodkyy Bjarne Stroustrup Texas A&M University, College Station, TX 77843-3112 [email protected], {yuriys, bs}@cs.tamu.edu

Abstract two or more arguments (“”), such as the binary Multiple dispatch – the selection of a function to be invoked based method problem [8]. Another problem is that dynamically dis- on the dynamic type of two or more arguments – is a solution to patched functions have to be declared within class definitions. This several classical problems in object-oriented programming. Open is intrusive and often requires more foresight than class designers multi-methods generalize multiple dispatch towards open-class ex- possess, complicating maintenance and limiting the extensibility tensions, which improve separation of concerns and provisions for of libraries. Open-methods provide an abstraction mechanism that retroactive design. We present the rationale, design, implementa- solves these problems by separating operations from classes. tion, and performance of a language feature, called open multi- Workarounds for both of these problems exist for single- methods, for C++. Our open multi-methods support both repeated dispatch languages. In particular, the (double dis- and virtual inheritance. Our call resolution rules generalize both patch) [18] circumvents these problems without compromising dispatch and overload resolution semantics. Af- type safety. Using the visitor pattern, the class-designer provides ter using all information from argument types, these rules can re- an accept method in each class and defines the interface of the vis- solve further ambiguities by using covariant return types. Great itor. This interface definition, however, limits the ability to intro- care was taken to integrate open multi-methods with existing C++ duce new subclasses and hence curtails program extensibility [12]. language features and rules. We describe a model implementa- In [37] Visser presents a possible solution to the extensibility prob- tion and compare its performance and space requirements to exist- lem in the context of visitor combinators, which make use of RTTI. ing open multi-method extensions and workaround techniques for Providing for multiple arguments avoids the C++. Compared to these techniques, our approach is simpler to use, restrictions of . When declared within classes, such catches more user mistakes, and resolves more ambiguities through functions are often referred to as “multi-methods”. When declared link-time analysis, runs significantly faster, and requires less mem- independently of the type on which they dispatch, such functions ory. In particular, the runtime cost of calling an open multimethod are often referred to as open class extensions, accessory func- is constant and less than the cost of a double dispatch (two virtual tions [39], arbitrary multi-methods [28], or “open-methods”. Lan- function calls). Finally, we provide a sketch of a design for open guages supporting multiple dispatch include CLOS [32], MultiJava multi-methods in the presence of dynamic loading and linking of [12, 27], Dylan [30], and Cecil [10]). We implemented and mea- libraries. sured both multi-methods and open-methods. Since open-methods address a larger class of design problems than multi-methods and Categories and Subject Descriptors [3]: 3 are not significantly more expensive in time or space, our discus- sion concentrates on open-methods. General Terms Design, Languages, Performance Generalizing from single dispatch to open-methods raises the Keywords multi-methods, open-methods, multiple dispatch, object- question how to resolve function invocations when no overrider oriented programming, , C++ provides an exact type match for the runtime-types of the argu- ments. Symmetric dispatch treats each argument alike but is sub- ject to ambiguity conflicts. Asymmetric dispatch resolves conflicts 1. Introduction by ordering the argument based on some criteria (e.g.: an argu- Runtime polymorphism is a fundamental concept of object-oriented ment list is considered left-to-right). Asymmetric dispatch seman- programming (OOP), typically achieved by late binding of method tics is simple and ambiguity free (if not necessarily unsurprising invocations. “Method” is a common term for a function chosen to the programmer), but it is not without criticism [9]. In addition, through runtime polymorphic dispatch. Most OOP languages (e.g.: asymmetric dispatch differs radically from C++’s symmetric func- C++ [34], Eiffel [26], Java [3], Simula [6], and [20]) tion overload resolution rules. use only a single parameter at runtime to determine the method to We derive our design goals for the open-method extension from be invoked (“single dispatch”). This is a well-known problem for the C++ design principles outlined in [33]. For open-methods, this operations where the choice of a method depends on the types of would mean the following. Open-methods should address several specific problems, be more convenient to use than all workarounds (e.g. the visitor pattern) as well as outperform them in both time and space. They should neither prevent separate compilation of translation units nor increase the cost of ordinary virtual function This is the author’s version of the work. It is posted here by permission of ACM calls. Open-methods should be orthogonal to exception handling for your personal use. Not for redistribution. The definitive version was published in order to be considered suitable for hard real-time systems (e.g. in Proceedings of the 6th international conference on Generative programming and component engineering, 2007. http://doi.acm.org/10.1145/1289971.1289993 [25]), and parallel to the virtual and overload resolution semantics. GPCE’07, October 1–3, 2007, Salzburg, Austria. The contributions of this paper include: Copyright c 2007 ACM 978-1-59593-855-8/07/0010. . . $5.00 • A design of open-methods that is coherent with C++ call- mal conversion algorithm must be chosen based on a source-target resolution semantics. pair of formats [22] [41]. • An efficient implementation and performance data to support 2.3 Compiler Pass over an AST its practicality. High-level compiler infrastructures ( [35], [29]) typically use ab- • A first known consideration of repeated and virtual inheritance stract syntax trees (AST) to represent programs. OOP enables mod- for multi-methods. eling the language constructs in an oo-hierarchy. Then, storing • A novel idea of harnessing covariance of the return type for pointers to a base class allows the use of several classes in the same ambiguity resolution. context. For example, a statement class would be the common base • for selective (e.g.: if or switch) and iterative constructs e.g.: for or A discussion of handling open-methods in the presence of dy- while. Analysis or transformation passes that would take the se- namic linking. mantics of the statement into account (e.g.: dataflow framework) need to uncover the real type. This is typically implemented with 2. Application Domains visitors or type-tags used to cast to a derived class. Open-methods Whether open-methods address a sufficient range of problems to be are a non-intrusive technique to write these compiler passes. They a worthwhile language extension is a popular question. We think guarantee type-safety, and allow retroactive extension of the oo- they do, but do not consider the problem one that can in general be hierarchy to support new language features or dialects. settled objectively, so we just present examples that would benefit significantly. We consider them characteristic for larger classes of 3. Definition of Open-methods problems. Open-methods are dynamically dispatched functions, where the 2.1 Shape Intersection callee depends on the dynamic type of one or more arguments. ISO C++ supports compile-time (static) on an ar- An intersect operation is a classical example of multi-methods us- bitrary number of arguments and runtime (dynamic) dispatch on a age [33]. For a hierarchy of shapes, intersect() decides if two single argument. The two mechanisms are orthogonal and comple- shapes intersect. Handling all different combinations of shapes (in- mentary. We define open-methods to generalize both, so our lan- cluding those added later by users) can be quite a challenge. guage extension must unify their semantics. Our dynamic call res- Worse, a programmer needs specific knowledge of a pair of shapes olution mechanism is modeled after the overload resolution rules of to use the most specific and efficient algorithm. C++. The ideal is to give the same result as static resolution would Using the multi-method syntax from [33], with virtual indicat- have given had we known all types at . To achieve this, ing runtime dispatch, we can write: we treat the set of overriders as a viable set of functions and choose bool intersect(virtual Shape&, virtual Shape&); // open−method the single most specific method for the actual combination of types. bool intersect(virtual Rectangle&, virtual Circle&); // overrider We derive our terminology from virtual functions: a function declared virtual in a base class (super class) can be overridden in a We note that for some shapes, such as rectangles and lines, derived class (sub class): the cost of double dispatch can exceed the cost of the intersect • algorithm itself. an open-method is a non-member function with one or more parameters declared virtual 2.2 Data Format Conversion • an overrider is an open-method that refines another open- Consider an image format library, written for domains such as im- method according to the rules defined in §3.1 age processing or web browsing. Conversion between different rep- • an open-method that does not override another open-method is resentations is not among the core concerns of an image class and called a base-method. a designer of a format typically can’t know all other formats. De- signing a class hierarchy that takes aspects like this into account For example: is hard particularly when these aspects depend on polymorphic be- struct A { virtual ˜A(); } a; havior. In this case, generic handling of formats by converting them struct B : A {} b; to and from a common representation in general gives unacceptable void print(virtual A&, virtual A&); // (1) performance, degradation in image quality, loss of information, etc. void print(virtual B&, virtual A&); // (2) Optimal conversion between different formats requires knowledge void print(virtual B&, virtual B&); // (3) of exact source and destination types, therefore it is desirable to have open-class extensions in the language, like open-methods. Here, both (2) and (3) are overriders of (1), allowing us to resolve Here is the top of a realistic image format hierarchy: calls involving every combination of A’s and B’s. For example, a call print(a,b) will involve a conversion of the B to an A and invoke Image (1). This is exactly what both static overload resolution and double dispatch would have done. RasterImage VectorImage To introduce the role of multiple inheritance, we can add to that example:

CompressedImage RandomAccessImage struct X { virtual ˜X(); }; struct Y : X, A {}; LossyImage LoslessImage YUV RGB CMYK void print(virtual X&, virtual X&); // (4) void print(virtual Y&, virtual Y&); // (5) PlanarYUV PackedYUV TrueColorRGB PalletizedRGB Here (4) defines a new open-method print on the class hierarchy rooted in X. Y inherits from both A and X, and since both print A host of concrete image formats such as RGB24, JPEG, and open-methods have the same signature, – (5) is an overrider for planar YUY2 will be represented by further derivations. The opti- both (4) and (1). 3.1 Overriding Our relaxed rule, however, does not reject this call as ambiguous at compile time, because these overriders have a unique base-method DEFINITION 1. An open-method is considered an overrider (or) for an open-method (om) in the same translation unit if it has the through which the call can be dispatched – 2. same name, the same number of parameters, covariant virtual pa- At link time, when all the overriders have been seen, we check rameter types, invariant non-virtual parameter types, and a possi- the overriders for return type consistency, perform ambiguity reso- bly covariant return-type. lution and build the dispatch tables. Runtime dispatch simply looks up the entry in the dispatch A base-method must be declared before any of its overriders. table that corresponds to the dynamic types of the arguments and This restriction parallels other C++ rules and greatly simplifies dispatches to that function. compilation. As shown in the previous example, an overrider can This three-stage approach parallels the resolution to the equiv- be associated with more than one base-method. alent modular-checking problem for template calls using concepts For every overrider and base-method pair, the compiler checks, in C++0x [21]. Further, the use of open-methods (as opposed to or- if the exception specifications and covariant return type (if present) dinary virtual functions and multi-methods) can be seen as adding comply with the semantics used for virtual functions. a runtime dimension to generic programming [4]. DEFINITION 2. An open-method that is not an overrider and an 3.2 Ambiguity Resolution overrider that introduces a covariant return type are considered a ++ base-method for a translation unit. C supports single-, repeated-, and virtual inheritance:

DEFINITION 3. A Dispatch table (DT) maps the type-tuple of the A A A A base-method’s virtual parameters to actual overriders that will be called for that type-tuple. B C B C B C Millstein and Chambers show in [28] that open-methods cannot be modularly type checked if the language (like C++) supports multiple implementation inheritance. Therefore, we split our call D D D resolution mechanism into three distinct stages: Note that to distinguish repeated and virtual inheritance, this di- • Overload resolution agram represents sub-object relationships, not just sub-class rela- • Ambiguity resolution tionships. We must handle all ambiguities that can arise in all these • cases. By “handle”, we mean resolve or detect as errors. Runtime dispatch Our ideal for resolving open-method calls combines the ideals The goal of overload resolution is to find at compile time a unique for virtual functions and overloading: base-method, through which the call can be dispatched. This base- • virtual functions: the same function is called independently of method determines a dispatch table through which the call will which class in an inheritance hierarchy is used in the call. be made, the necessary casts of the arguments, and the expected • return type. The actual overrider to handle the call is determined at overloading: a call is considered unambiguous if (and only runtime. if) every parameter is at least as good a match for the actual The C++ overload resolution rules [23] are unchanged: the vi- argument as the equivalent parameter of every other candidate able set includes both open-methods and regular functions and function and that it has at least one parameter that is a better treats open-methods like any other freestanding functions. Follow- match than the equivalent parameter of every other candidate ing the static rules exactly would imply that a base-method is used function. only if an open-method is the best match. We can do slightly better; This implies that a call of a single-argument open-method is re- that is, we can resolve some static ambiguities at link time (where solved equivalently to a virtual function call. The rules described in more information is available): if a set of best matches consists of this paper closely approximate this ideal. As mentioned, the static open-methods only and the intersection of their base-methods has resolution is done exactly according to the usual C++ rules. The dy- a single element - overload resolution does not report an ambiguity. namic resolution is presented as the algorithm for generating dis- We demonstrate with an example: patch tables in §3.4. Before looking at that algorithm, we present struct X; some key motivating examples. struct Y; 3.2.1 Single Inheritance struct Z; In object models supporting single inheritance (§3.2) ambiguities void foo(virtual X&, virtual Y&); // (1) can only occur with open-methods taking at least two virtual pa- void foo(virtual Y&, virtual Y&); // (2) rameters. Ambiguities in this case have to be resolved by intro- void foo(virtual Y&, virtual Z&); // (3) ducing a new overrider. The resolution of an open-method with struct XY : X, Y {} xy; one argument is identical to that of a virtual function. Thus, open- struct YZ : Y, Z {} yz; methods provide an unsurprising mechanism for expressing non- void foo(virtual XY&, virtual Y&); // (4) intrusive (“external”) polymorphism. This eliminates the need to void foo(virtual Y&, virtual YZ&); // (5) complicate a class hierarchy just to support the later addition of additional “methods” in the form of visitors. Open-methods 1,2 and 3 are three independent base-methods defined on different class hierarchies. Because XY and YZ are 3.2.2 Repeated Inheritance parts of several hierarchies, overriders 4 and 5 refine several base- Consider the repeated inheritance case (§3.2) together with this set methods. In particular, 4 is an overrider for 1 and 2, while 5 is an of open-methods visible at a call site to foo(d1,d2): overrider for 2 and 3. A call foo(xy,yz); is now ambiguous according to the standard overload resolution rules as both 4 and 5 are equally good matches. void foo(virtual A&, virtual A&); We cannot detect the ambiguities marked with ?? at compile void foo(virtual B&, virtual B&); time, but we can catch them at link time when the full set of void foo(virtual B&, virtual C&); overriders are known. void foo(virtual C&, virtual B&); void foo(virtual C&, virtual C&); 3.3 Covariant Return Types Covariant return types are a useful element of C++. If anything, Even tough the compiler can determine a unique base-method they appear to be more useful for operations with multiple argu- foo(A&, A&) through which the call can be dispatched, the call ments than for single argument functions. Covariant return types with two arguments of type D gets rejected at compile-time. The complicate the use of workaround techniques. In case of the visi- problem in this case, is that there are multiple pathes from D to tor, it would require even more foresight and lead to a proliferation sub-objects of type A. of accept methods that have to be replicated in each derived class. To resolve that conflict, a user can either add an overrider As an example for using covariant return type, consider a class foo(D&,D&) visible at the call site or explicitly cast arguments to Symmetric derived from Matrix: either the B or C sub-object. Making an overrider for foo(D&,D&) available at the call site, renders the choice of the path to the Matrix& operator+(Matrix&, Matrix&); subobject irrelevant. It would always be dispatched to the same Symmetric& operator+(Symmetric&, Symmetric&); overrider. It follows that we must generalize the covariant return rules for If the (B,C)-vs.-(C,B) conflict is resolved by casting, a question open-methods. Doing so turns out to be useful because covariant remains on how the linker should resolve a call with two arguments return types help resolve ambiguities. of type D? We know at runtime (by looking into the virtual function In single dispatch, covariance of a return type implies covari- table’s open-method table (see §4) which “branch” of a D object ance of the receiver object. Consequently, covariance of return (either B or C) is on. Thus, we can fill our dispatch table appropri- types for open-methods implies an overrider (or) - base-method ately; that is, for each combination of types, there is a unique “best (bm) relationship between two open-methods. Liskov’s substitu- match” according to the usual C++ rules: tion principle [24] guarantees that any call type-checked based on A B C D/B D/C bm can use or’s covariant result without compromising type safety. This can be used to eliminate what would otherwise have been A AA AA AA AA AA ambiguities. Consider the class hierarchies A ← B ← C and B AA BB BC BB BC R1 ← R2 ← R3 together with this set of open-methods: C AA CB CC CB CC D/B AA BB BC BB BC R1∗ foo(virtual A&, virtual A&); D/C AA CB CC CB CC R2∗ foo(virtual A&, virtual B&); R3∗ foo(virtual B&, virtual A&); This depicts the dispatch table for the repeated-inheritance hierar- chy in §3.2 and the set of overriders above. Since the base method A call foo(b,b) appears to be ambiguous and the rules out- is foo(A&,A&) and A occurs twice in D, each dimension has two lined so far would indeed make it an error. However, choosing entries for D: D/B meaning ”D along the B branch”. This resolution R2∗ foo(A&,B&) would throw away information compared to us- exactly matches our ideals. ing R3∗ foo(B&,A&): An R3 can be used wherever an R2 can, but R2 cannot be used wherever an R3 can. Therefore, we prefer a 3.2.3 Virtual Inheritance function with a more derived return type and for this example get the following dispatch table: Consider the virtual inheritance class hierarchy from §3.2 together with the set of open-methods from §3.2.2: In contrast to repeated A B C inheritance, a D has only one A part, shared by B, C, and D. A AA AB AB This causes a problem for calls requiring conversions, such as B BA BA BA foo(b,d); is that D to be considered a B or a C? There is not enough C BA BA BA information to resolve such a call. Note that the problem can arise At first glance, this may look useful, but ad hoc. However, an is such a way that we cannot catch it at compile time: open-method with a return type that differs from its base method becomes a new base method and requires its own dispatch table (or C& rc = d; equivalent implementation technique). The fundamental reason is foo(b,rc); the need to adjust the return type in calls. Obviously, the resolutions B& rb = d; for this new base method must be consistent with the resolution foo(b,rb); for its base method (or we violate the fundamental rule for virtual Using static type information to resolve either call would violate functions). However, since R2∗ foo(A&,B&) will not be part of the fundamental rule for virtual function calls: use runtime type R3∗ foo(B&,A&)’s table, the only consistent resolution is the one information to ensure that the same overrider is called from every we chose. point of a class hierarchy. At runtime, the dispatch mechanism will If the return types of two overriders are siblings, then there is (only) know that we are calling foo with a B and a D. It is not known an ambiguity in the type-tuple that is a meet of the parameter- whether (or when) to consider that D a B or a C. Based on this type tuples. Consider for example that R3 derives directly from reasoning (embodied in the algorithm in §3.4) we must generate R1 instead of R2, then none of the existing overriders can be used this dispatch table: for (B,B) tuple as its return type on one hand has to be a subtype of R2 and on the other a subtype of R3. To resolve this ambiguity, the A B C D/A user will have to provide explicitly an overrider for (B,B), which A AA AA AA AA must have the return type derived from both R2 and R3. B AA BB BC ?? Using the covariant return type for ambiguity resolution also C AA CB CC ?? allows the programmer to specify preference of one overrider over D/A AA ?? ?? ?? another when asymmetric dispatch semantics is desired. To conclude: covariant return types do not only improve static Algorithm 1 Dispatch Table Generation type information, but also enhance our ambiguity resolution mech- S ← topological sort(X) anism. We are unaware of any other multi-method proposal using a for all x ∈ S do similar technique. if x ∈ Y then 3.4 Algorithm for Dispatch Table Generation DT [x] ← F (x) else Let us assume we have a multi-method rf(h1, h2, ..., hk) with max set = {B1(x)} th k virtual arguments. Class hi is a base of hierarchy of the i for i ← 2, k do argument. Hi = {c : c <: hi} is a set of all classes from the dominated ← false hierarchy rooted at hi. X = H1 × H2 ×···× Hk is the set of all for all e ∈ max set do −1 −1 possible argument type-tuples of f. Set Y = {(y1, y2, · · · , yk)} ⊆ if F (DT [e])

1 1 computations on numbers that are bound by the product of these AA inD1 A B AA inD2 A B primes. Such product can fit into 32-bit integers for only the 9 A AA1 AB2 A AA1 AA1 first primes and into 64-bit integers for only the 15 first primes. B AA1 AB2 B AA1 BB1 Table compression techniques [2] or use of minimal perfect hash functions [14] instead, can help overcome the problem.

When both modules are loaded together, we get the dilemma 5. Dynamic Linking of how to resolve a call with both arguments of type B. On one side foo(B&,B&) from D2 is more specialized, but on the other Outside embedded systems, dynamically linked libraries are almost side foo(A&,B&) from D1 imposes the additional requirement that universally used with C++. Thus, a design for open-methods that the return type of whatever is called for (B,B) should be a subtype does not allow for DLLs is largely theoretical. We do not currently of R2, which R1 is not. Such scenario would have been rejected have an implementation supporting dynamic linking, but we can should it happen at compile/link time, however at load time we do outline a design addressing the major issues for open-methods in a not have this option anymore. dynamically linked library. It guarantees that the most specialized Keeping all dispatch tables of a particular open-method consis- overrider available at runtime that preserves type-safety of a call tent on the overrider that will be called for a particular combination will be used to dispatch a call. of types will force us to choose between suboptimal and type un- Dynamic modules, compiled independently, may have different safe alternatives. What is worse - is that there may not be a unique sets of overriders defined at the time of compilation. Furthermore, type-safe alternative. there could be new classes added to a hierarchy in one of the mod- Imagine for example that a module D3 introduces an overrider ules and objects of those classes may be passed into code of other R3 foo(B&, A&) where R1 ← R3, so R2 and R3 are siblings. modules. This is not a problem for regular virtual functions as their When D1 and D3 are loaded together, neither R2 foo(A&, B&) v-tables are found in the module where the class was defined. In nor R3 foo(B&, A&) can be used to resolve a call with both case of open-methods, the dispatch table generated within a partic- arguments of type B - both alternatives are type unsafe for the other ular module can be simply unaware of a class, defined somewhere overrider. else. To account for this, the dynamic loader will have to update To deal with this subtlety, we propose to weaken for the DLL dynamically dispatch tables of each open-method as new modules case the requirement that the same overrider should be called for are loaded and more information becomes available. the same tuple of dynamic types regardless of the static types used Covariant return introduces subtleties when dynamic linking is at the call site. We require that the same overrider be used only used. Consider a two-class hierarchy A ← B and another two-class if it is type-safe for the caller. Strictly speaking R1 foo(B&,B&) hierarchy R1 ← R2. The base-method R1 foo(virtual A&, virtual A&)is not an overrider of R2 foo(A&, B&) as defined in §3.1, be- is defined in a header visible by two dynamically linked modules cause its return type is not changing covariantly in respect to the D1 and D2 that do not know anything about each other. Module D1 types of arguments. Therefore, it cannot be considered for the dy- introduces overrider R2 foo(A&, B&) and module D2 introduces namic resolution of calls made statically through the base-method overrider R1 foo(B&, B&). Each of the dynamically linked mod- R2 foo(A&, B&). ules perfectly type-checks and links with foo() resolved through Taking the above into account, we propose that the dynamic the dispatch table (a superscript in a cell denotes the type that is linker fills in the dispatch table of every base-method indepen- returned by an overrider e.g. AB2 denotes R2 foo(A&, B&)): dently. This results in: 1 2 3 In principle, both handleEvent functions should correctly han- AA A B AB B BA A B dle the event; that is, each handleEvent function must assume that A AA1 AB2 A AB2 its argument type is just base class and the actual argument could B BA3 BB1 B AB2 B BA3 BA3 be of an unknown derived class. Consequently, each handleEvent function must be written in a way that is generic on its argument It looks as if the dispatch table for the base-method R1 foo(A&,A&) (probably using virtual functions on the individual argument). This now violates covariant consistency, but in reality it does not because implies that as long as an event handler’s code does not make more all the return types in it are cast back through thunks to R1, which assumptions about its arguments than the interface defined in the is the type statically expected at the call site. base-class guarantees, it can be replaced by the other event handler. As can be seen, this logic may result in different functions being Even a non-deterministic selection of the overrider would produce called for the same type tuple depending on the base-methods seen a correct result! However, developers and testers strongly prefer at the call site. We note, however, that the call is always made to deterministic choices, so we consistently choose one of the alterna- the most specialized overrider that is type-safe for the caller. tives. Furthermore, as we have already mentioned, two DLLs may Even when covariant return types are not used, dynamic linking provide different implementations for the same overrider, in which can introduce ambiguities in the dispatch tables. The simplest ex- case we have to make a choice. ample would be to think of two different modules that both provide With this said we propose to resolve ambiguities at load time as overrider for the same pair of dynamic types. following. For each base-method of a given open-method: Let us consider a plausible scenario involving three DLLs: • If there is a unique best match among type-safe overriders that // dll−1 can handle a particular combination of argument types – use it. struct GuiButton { virtual ˜GuiButton(); }; struct GuiEvent { virtual ˜GuiEvent(); }; • If there is no unique best match, – make a deterministic choice among all best matches. void handleEvent(virtual GuiButton&, virtual GuiEvent&); // dll−2 We chose an unspecified determinism over non-determinism to #include guarantee that the same method will always be selected and to keep struct MyButton : GuiButton {}; the dynamic dispatch symmetric. void handleEvent(virtual MyButton&, virtual GuiEvent&); 6. Related Work // dll−3 #include Programming languages can support multi-methods either through struct SpecialEvent : GuiEvent {}; built-in facilities, pre-processors, or through library extensions. Naturally, tighter language integration enjoys a much broader de- void handleEvent(virtual GuiElement&, virtual SpecialEvent&); sign space for type checking, ambiguity handling, and optimiza- The first DLL defines a class GuiButton, a class GuiEvent, tions compared to libraries. In this section, we will first review both and a base-multi-method handleEvent. Internally, a second DLL library and non-library approaches for C++ and then give a brief derives a new type MyButton from GuiButton and introduces a overview of multi-methods in other languages. new overrider for handleEvent. Likewise, the third DLL derives a new internal class SpecialEvent from GuiEvent and introduces a 6.1 Cmm new overrider. The second and third DLL could stem from different Cmm [31] is a preprocessor based prototype implementation for vendors that do not know about each other. an open-method C++ extension. It takes a translation unit and Now a call of handleEvent with a MyButton and a SpecialEvent generates C++ dispatch code from it. Cmm is available in two is ambiguous. The writer of the total system (the “system integra- versions. One of them uses RTTI to recover the dynamic type of tor”) should in principle have considered that possibility and dealt objects to identify the best overrider. The other approach achieves with it. Therefore, one solution would be to terminate the program constant time dispatch by relying on a virtual function overridden in or to throw an exception [31]. However, such problems are hard to each class. Dispatch ambiguities are signalled by throwing runtime predict and design for. Relaxed Multi-Java [27] resolves these con- exceptions. Cmm allows dynamically linked libraries to register flicts by introducing glue methods (to glue DLL2 and DLL3) that and unregister their open-methods at load and unload time. In the system-integrator provides. While this might be a viable solu- addition to open-method dispatch, Cmm also provides call-site tion for software developers integrating several libraries, it is not a virtual dispatch. Cmm does not provide special support for multiple feasible scenario for end-user applications, as dynamically linked inheritance and therefore its dispatch technique does not entirely modules can be loaded into the process without direct request of a conform with virtual function semantics. developer. This, for example, is the case with various component object models when application may ask the system to create an 6.2 DoubleCpp object with a particular name and operating system will locate and load the module it is resided in. DoubleCpp [5] is another preprocessor based approach for multi- We emphasize that in current C++, it is not possible to write methods dispatching on two virtual parameters. It essentially trans- programs against both types without getting an ambiguity. We lates these multi-methods into the visitor pattern. For doing so, report such an ambiguity when it is detected at compile or link DoubleCpp requires access to the files containing the class defi- time, however, at load time we do not have such an option as it is nitions in order to add the appropriate accept and visit methods. detected on a user’s machine. DoubleCpp, like any other visitor-based approach, does not report One way to handle such a scenario will be to treat both classes but quietly resolve ambiguities. as their base classes and dispatch appropriately. We note, however, that in the plug-in like usage scenarios modules may have only seen 6.3 Accessory Function the interface: the base-multi-method and the roots of the hierarchies The accessory functions papers [16, 39] allow open method dis- on which it is defined. Nevertheless, such use-cases would expect patch based on a single virtual argument and discuss ideas to extend more refined overriders to handle calls on derived classes. the mechanism for multiple dispatch. The compilation model they describe uses, like our approach, a compiler and linker coopera- chooses 2 out of 32 objects and invokes the intersect method. We tion to perform ambiguity resolution and dispatch table generation. implemented a table-based random number generator that is sim- However, the accessory functions are integrated into the regular v- ple and does not contain any floating-point calculations or integer- tables of their receiver types, which requires the linker to not only divisions. We ran the loop twice with the same random numbers: generate the dispatch table but also to recompute and resolve the The first run allows implementations, which build the dispatch data v-table index of any other virtual member function. Both papers do structure on the fly to warm up and load data/code into the cache. not provide a detailed discussion of the intricacies when multiple The second loop was timed. The clock-cycle based timer takes the inheritance is involved. The authors do not refer to a model or pro- time before and after the loop and we calculate the average number totype implementation to which we could compare our approach. of clock-cycles per loop to compare the results.

6.4 Loki 7.1 Implementations Loki [1], based on Alexandrescu’s template programming library We tested the approaches on a Pentium D, 2.8 Ghz running CentOS with the same name, provides several different dispatchers that Linux and a Core2Duo running Mac OSX. The code was compiled balance between speed, flexibility, and code verbosity. Currently, with g++ 4.0 (Linux) and gcc 4.0.1 (OSX) with optimization level it supports multi-methods with two arguments only, except for the set to -O3. EDG/Omm generates lowered to C, which constant-time dispatcher that allows more arguments. The static was compiled with the corresponding gcc versions and linked to dispatcher provides call resolution based on overload resolution the pre-linker generated dispatch tables. rules, but requires manual linearization of the class hierarchy in Using the Chinese Remainder approach, the number associated order to uncover the most derived type of an object first. All other with the dispatch table grows exponentially with the number of dispatchers do not consider hierarchical relations and effectively types. Therefore the test is limited to eight types instead of 20 and require explicit resolution of all possible cases. the size of the executable is omitted. We could only implement a simplified version that can handle eight types instead of 20. Hence, 6.5 Other Languages we omit the size of the program executable. One of the first widely known languages to support multi-methods For Loki, we only tested the static dispatcher because the other was CLOS [32]. CLOS linearizes the class hierarchy and uses require manual handling of all possible cases. Using other dispatch- asymmetric dispatch semantics to avoid ambiguity conflicts. Ce- ers would have been closer to a scenario of manually allocated array cil [9, 10] views silent ambiguity resolution as a potential source of functions through which calls are made. However, as we indi- for programming errors. Therefore, it uses symmetric dispatch se- cated before, the dual nature of multi-methods require them to pro- mantics and dispenses with object hierarchy linearization in or- vide both dynamic dispatch and automatic resolution mechanism. der to expose these errors at compile-time. In [28], Millstein and Chambers discuss the trade-offs between multi-methods and mod- 7.2 Results & Interpretation ular type-checking in languages with neither a total order of classes Our experimental results can be summarized in terms of execution nor asymmetric dispatch semantics. Ranging from globally type- time and program size: checked programs to modularly type-checked units, the models embrace or restrict the expressive power of the language to dif- Approach Size (bytes) Cycles/Loop Cycles/Loop ferent degrees. Based on these findings, MultiJava [12] implements Linux Pentium-D Core2Duo a model that allows separate compilation and eliminates the need Virtual function n/a 75 55 for a link-time type-checker but also curtails extensibility. Relaxed C++ Multi-method 19 547 78 60 MultiJava [27] re-introduces a link-time type checker and relies on C++ Open-method 19 725 82 63 the system integrator to resolve ambiguities by providing new over- EDG/Omm 70 647 82 64 riders (glue-methods). Parasitic methods model multi-methods as Double Cpp 20 859 120 82 class members and give the receiver precedence over other argu- C++ Visitor 35 289 132 82 ments. Implementations for Java [7] and Smalltalk [17] exist. An Chinese Remainders n/a 175 103 example of a language adding multi-methods through a library is Cmm (constant time) 112 250 415 239 Python [36]. Chambers and Chen [11] present an alternative imple- Cmm 111 305 1 320 772 mentation technique based on a lookup DAG. Their work general- Loki Library 34 908 3 670 2 238 izes multiple dispatch to be a subset of predicate-based dispatch.

Executable size: We present the executable size obtained on 7. Results the Linux system. The size of dispatch tables is mentioned as one of In order to discuss time and space performance, we compare a the major drawbacks of providing multi-methods as programming number of handcrafted implementations with EDG/Omm, our im- language feature [39]. However, our results reveal that the visitor plementation described in §4, Cmm, DoubleCpp, and the Loki that is based on a brute force implementation is 80% bigger than library. The handcrafted approaches include multi-methods and the multi-method approach. With the visitor, each shape class has open-methods, model implementations used to initially assess the intersect methods for all 20 shapes of the hierarchy. A somewhat performance trade-offs, a Chinese Remainder (§4.3) based imple- smarter approach would be to remove redundant intersect overrid- mentation, and the visitor pattern. ers. However, removing specific overriders is tedious and difficult We wrote 20 classes (representing shapes, etc.) which can inter- to maintain, since the dispatch would be based on the static type sect each other. Overall, this results in 400 combinations for binary information of the base class. Even an optimized approach would dispatch functions. We implemented 40 specific intersect functions be at best able to match the multi-method implementation, simply to which all of the 400 combinations are dispatched to. In order because each type contains 20 intersect entries in the v-table. Mul- to get a reliable timing of the function invocation, these 40 inter- tiplying this with the number of shapes, 20, results in 400, exactly sect functions only increment a counter. Since not all techniques the number of entries found in the dispatch table. The executable we use support multiple inheritance, these 20 classes only use sin- generated by EDG/Omm is bigger because it is statically linked to gle inheritance. The actual test consists of a loop that randomly EDG’s C++ runtime library. We do not discuss the program size of the two Cmms and Loki, since they use additional header files such linker technique described here should be able to synthesize v- as and that distort a direct comparison. tables for virtual function templates as it does for open-methods. Execution time: Multi-Methods, Open-Methods, and EDG/Omm are (as expected) roughly comparable to a single virtual function 8.2 Function Pointers to Open-methods dispatch, which needs 75 (55 on the Core2Duo) cycles per loop. Pointers to open-methods would generalize pointer to member- Hence, the better performance compared to the visitors is not sur- functions. This allows to separate n-ary open-methods from their prising. However, the fact that multi-methods reduce the runtime arguments and write code abstracted from a concrete operation. to 62% (73%) of the reference implementation using the visitor is noteworthy. We conjecture this is an effect of the size of the class hierarchy and that the time to double dispatch depends on the 8.3 Calling a Base Implementation number of overriders. On the Pentium D, two observations support C++ provides a syntax to call a particular base implementation our conjecture: firstly, the DoubleCpp-based visitor has no redun- of a virtual member function directly, avoiding dynamic dispatch. dant overriders and runs slightly faster. Secondly, we simulated an This is often used to call the function in the base class. To do analysis pass dispatching over AST-objects of 20 different types this, C++ requires the user to use a fully qualified name of virtual and counting the category to which they belong (type, declaration, member function: e.g.: p−>MyClass::foo(); It is likely that similar expression, statement, other). In this case, the double dispatch has functionality will be required for open-methods. only 20 leaf-functions instead of 400 and our dispatch test runs 78 cycles instead of 132. The open-method approach requiring only 8.4 Space Optimizations five overriders, is still faster and needs 68 cycles. With large class hierarchies, the size of dispatch tables can become The difference between multi-methods and open-methods (EDG/Omm) significant, especially when we consider support for covariant re- is within the expected range. Three more indirections require 4 (4) turn types. The implementation of space optimizations techniques more clock cycles on the Pentium and 3 (4) more on the Core2Duo. to compressing and reusing of dispatch tables [2] would reduce the Although significantly slower, Cmm (constant time) performs bet- memory footprint. ter than expected, since its author estimates the dispatch cost as 10 times a regular virtual function call. As expected the two non- constant time approaches perform worst. References Significance of performance: The performance numbers comes from experiments designed to highlight the cost of multi- [1] A. Alexandrescu. Modern C++ Design: Generic Programming and Design Patterns Applied. AW C++ in Depth Series. Addison Wesley, ple dispatch: the functions invoked hardly do anything. Depending January 2001. on the application the improved performance might or might not be significant. For the image conversion example, gains in exe- [2] E. Amiel, O. Gruber, and E. Simon. Optimizing multi-method cution speed are negligible compared to time spent in the actual dispatch using compressed dispatch tables. In OOPSLA ’94, pages 244–258, New York, NY, USA, 1994. ACM Press. conversion algorithm. In other cases, such as the evaluation of ex- pressions using user-defined arithmetic types, traversal of abstract [3] K. Arnold, J. Gosling, and D. Holmes. The Java Programming syntax trees, and some of the most frequent shape intersect exam- Language, 3rd edition. Addison-Wesley Longman Publishing Co., ples, the speed differences among the double dispatch approaches Inc., Boston, MA, USA, 2000. appear to be notable. [4] M. H. Austern. Generic programming and the STL: using and Contrary to much “popular wisdom”, our experiments revealed extending the C++ Standard Template Library. Addison-Wesley that for many applications the use of dispatch tables for open- Longman Publishing Co., Inc., Boston, MA, USA, 1998. methods and multi-methods actually reduce the program size com- [5] L. Bettini, S. Capecchi, and B. Venneri. Double dispatch in C++. pared to brute-force and work-around techniques. Software - Practice and Experience, 36(6):581 – 613, 2006. [6] G. M. Birtwistle, O. Dahl, B. Myhrhaug, and K. Nygaard. Simula 8. Conclusions and Future Work BEGIN. Auerbach Press, Philadelphia, 1973. [7] J. Boyland and G. Castagna. Parasitic methods: an implementation We presented a novel approach to dispatching open multi-methods of multi-methods for Java. In OOPSLA ’97, pages 66–76, New York, that is in line with the multiple inheritance semantics of the current NY, USA, 1997. ACM Press. ++ ++ C object model and the C overload resolution rules. This im- [8] K. Bruce, L. Cardelli, G. Castagna, G. T. Leavens, and B. Pierce. On plies compile-time or link-time detection of ambiguities. By con- binary methods. Theor. Pract. Object Syst., 1(3):221–242, 1995. sidering covariant return type in the ambiguity resolution, we re- duce the number of potential conflicts. We have discussed an im- [9] C. Chambers. Object-oriented multi-methods in Cecil. In ECOOP ’92, pages 33–56, London, UK, 1992. Springer-Verlag. plementation based on modifications to the EDG compiler front- end and have described a mechanism that supports the integration [10] C. Chambers. The Cecil language: Specification and rationale. 3.2. of several translation units. Our evaluation of different approaches Technical report, Department of Computer Science and Engineering. to implementing open-methods in C++ shows that our approach is University of Washington, 2004. significantly better (in time and space) than current alternatives. In- [11] C. Chambers and W. Chen. Efficient multiple and predicated deed, it is almost as efficient as single dispatch. Since the dispatch dispatching. In OOPSLA ’99, pages 238–255, New York, NY, USA, is constant time and does not rely on exceptions to signal ambigui- 1999. ACM Press. ties, it is applicable in embedded and hard real-time systems. [12] C. Clifton, G. T. Leavens, C. Chambers, and T. Millstein. MultiJava: Future plans to extend our work include: modular open classes and symmetric multiple dispatch for Java. In OOPSLA ’00, pages 130–145, New York, NY, USA, 2000. ACM 8.1 Virtual Function Templates Press. [13] Codesourcery.com. The Itanium C++ ABI. Technical report, 2001. Virtual function templates are a powerful abstraction mechanism not part of C++. Generating v-tables for virtual function templates [14] T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein. Introduction requires a whole-program view and C++ traditionally relies almost to algorithms. MIT Press, Cambridge, MA, USA, 2001. exclusively on separate compilation of translation units. The pre- [15] Edison Design Group. C++ Front End, March 2006. [16] C. B. Flynn and D. Wonnacott. Reconciling encapsulation and [29] M. Schordan and D. Quinlan. A source-to-source architecture for dynamic dispatch via accessory functions. Technical Report 387, user-defined optimizations. In JMLC’03, volume 2789 of LNCS, Rutgers University Department of Computer Science, 1999. pages 214–223. Springer-Verlag, August 2003. [17] B. Foote, R. Johnson, and J. Noble. Efficient Multimethods in a [30] A. Shalit. The Dylan Reference Manual. 2nd edition. Apple Press, Single Dispatch Language. Proceedings of the European Conference 1996. on Object-Oriented Programming, Glasgow, Scotland, July, 2005. [31] J. Smith. Draft proposal for adding multimethods to C++. Technical [18] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design patterns: Report N1463, JTC1/SC22/WG21 C++ Standards Committee, 2003. Elements of reusable object-oriented software. Addison-Wesley [32] G. L. Steele Jr. : the language (2nd ed.). Digital Press, Longman Publishing Co., Inc., Boston, MA, USA, 1995. Newton, MA, USA, 1990. [19] M. Gibbs and B. Stroustrup. Fast dynamic casting. Softw. Pract. [33] B. Stroustrup. The design and evolution of C++. ACM Exper., 36(2):139–156, 2006. Press/Addison-Wesley Publishing Co., New York, NY, USA, 1994. [20] A. Goldberg and D. Robson. Smalltalk-80: the language and its [34] B. Stroustrup. The C++ . Addison-Wesley implementation. Addison-Wesley Longman Publishing Co., Inc., Longman Publishing Co., Inc., Boston, MA, USA, 2000. Boston, MA, USA, 1983. [35] B. Stroustrup and G. Dos Reis. Supporting SELL for high- [21] D. Gregor, J. Jarvi, J. Siek, B. Stroustrup, G. D. Reis, and A. Lums- performance computing. In LCPC’05, volume 4339 of LNCS, pages daine. Concepts: Linguistic support for generic programming. OOP- 458–465. Springer-Verlag, October 2005. SLA’06, 2006. [36] G. van Rossum. The Python Language Reference Manual. Network [22] International Organization for Standardization. ISO/IEC 10918- Theory Ltd., September 2003. 1:1994: Information technology — Digital compression and coding of continuous-tone still images: Requirements and guidelines. 1994. [37] J. Visser. Visitor combination and traversal control. In OOPSLA ’01, pages 270–282, New York, NY, USA, 2001. ACM Press. [23] ISO/IEC 14882 International Standard. Programming languages C++. American National Standards Institute, September 1998. [38] D. Wasserrab, T. Nipkow, G. Snelting, and F. Tip. An operational semantics and type safety proof for multiple inheritance in C++. In [24] B. Liskov. Keynote address - data abstraction and hierarchy. In OOPSLA ’06, pages 345–362, New York, NY, USA, 2006. ACM OOPSLA ’87, pages 17–34, New York, NY, USA, 1987. ACM Press. Press. [25] Lockheed Martin. Joint Strike Fighter, Air Vehicle, C++ Coding [39] D. Wonnacott. Using accessory functions to generalize dynamic Standard. Lockheed Martin, December 2005. dispatch in single-dispatch object-oriented languages. In COOTS, [26] B. Meyer. Eiffel: The Language. Prentice-Hall, Inc., Upper Saddle pages 93–102. USENIX COOTS, 2001. River, NJ, USA, 1992. [40] A. Woß,¨ M. Loberbauer,¨ and H. Mossenb¨ ock.¨ LL(1) conflict [27] T. Millstein, M. Reay, and C. Chambers. Relaxed MultiJava: resolution in a recursive descent compiler generator. In JMLC’03, balancing extensibility and modular typechecking. In OOPSLA volume 2789 of LNCS, pages 192–201. Springer-Verlag, 2003. ’03, pages 224–240, New York, NY, USA, 2003. ACM Press. [41] www.fourcc.org. Video codec and pixel format definition, February [28] T. D. Millstein and C. Chambers. Modular statically typed 2007. multimethods. In ECOOP ’99, volume 1628 of LNCS, pages 279– 303, London, UK, 1999. Springer-Verlag.