Topic: Modular method combination (was: Virtual functions: should there be more than one kind?)
Author: Rick Busdiecker <rfb@lehman.com>
Date: Sat, 18 Sep 1993 23:15:47 GMT Raw View
Thanks for the replies. As I wasn't especially clear in what I was
proposing, I'll try to nail it down a bit. Also, I'll throw in that,
since making my earlier post, I've implemented a way to do modular
method combination that supports multiple inheritance. The approach
is a bit bleacherous, but it does allow you to specialize properly
defined member functions based soley on the immediate base classes of
the class that you're defining.
So, what I was proposing was actually two proposals. I believe that
they are completely independent in that either one could be adopted
without the other.
1. Add a keyword, say `combined' which could be used when declaring a
member function. A member specified as combined would be limited
in a manner similar to destructors. It could not accept any
arguments, nor return any values. Actually, I think that you
could relax the restriction on arguments, but let's go slowly here
at least at first. The semantics would also be similar to
destructors (except for the destruction part :-) When a combined
member function is invoked, every applicable member function in
the class hierarchy is invoked in a well defined order. This
ordering could be exactly the same as the one defined for
destructors. While I'd prefer the ordering used by CLOS's default
method combination, the only thing that I consider critical is
that a member function specific to a particular class is invoked
before any like named member function specific to any of that
class's base classes.
Combined methods may be virtual, however they do not necessarily
have to be.
Although the syntax is *very* different, this proposal is
essentially what I implemented when trying to find a modular way
to do method combination.
2. Add a syntax for explicitly invoking the next member function in
a class's method combination sequence. While, as Jacob suggested,
something called `call_next_method' probably isn't appropriate.
However, `inherited' really doesn't quite cut it either.
Consider, the ARM example with the following class hierarchy:
W
/ \
A B
\ /
C
When invoking f for a C object, the methods are invoked in the
following order: C, A, B, W. So the _f invoked immediately
after A::_f is B::_f although it doesn't really seem appropriate
to say that A inherits anything from B.
I was thinking that it might be fitting to define some new syntax
to attach to the member function name, say <>::f, and let people
call it whatever they want to :-)
This mechanism would be restricted only in that it could not be
used with constructors, destructors or `combined', as defined
above, member functions.
Depending on the implementation, this proposed mechanism is either
less static than the one in my first proposal or else it requires
an unacceptably massive increase in code size for all but the most
trivial hierarchies. However, the mapping of object class and
method class to next method class can be created during
compilation. A relatively straight forward implementation would
have attempts to invoke the `next method' do a run-time look up
which could be made fairly similar to way that virtual function
lookup is done currently. Of course this does mean that invoking
a next method when none existed would be a run-time error since
for a particular `method', the existance of the next method
depends on the class of the object on which it is being invoked.
What I implemented achieves modularity in the sense that it allows a
classes definition to be independent of it's indirect base classes.
As I've mentioned, the implementation is somewhat bleacherous. It
requires a parallel class hierarchy for each method. I tried to come
up with a way to limit it to a single parallel hierarchy parameterized
by the name of the member function, however I couldn't really come up
with a way to specify a like named member function defined on another
class in a generalized way. This is at least in part due to the fact
that there is no `class object' for C++ classes. There may be a way
to do this with a special static instance of each class, but if there
is, I haven't figured it out yet.
Anyway, the implementation takes advantage of the fact that
destructors provide modular method combination with a reasonable
ordering. It still requires that you define two functions, say f and
_f, where _f is the `method' and f is the `generic function'. f is
defined something like this:
void X::f ()
{
delete (new Xf (this));
}
The constructor for Xf looks something like this:
Xf::Xf (X* pointer)
{
set_cache ((void*)pointer);
}
and the destructor looks something like this:
Xf::~Xf ()
{
((X*)get_cache ())->_f ();
}
Where get_cache and set_cache are inherited from the root class of the
parallel hierarchy.
I used this approach to define a class hierarchy including 13 classes
and in addition to convincing myself that the approach works, I'm
pretty sure that I turned up a bug in the way that at least one C++
compiler orders destructor methods. I'll have to investigate a bit
further, but I believe that this is a Cfront derived compiler so the
bug may be fairly widespread, however it doesn't violate the
derived-before-base constraint so I doubt it's particular important.
In fact, I think it reinforces my position that that's the only really
critical property of a default method combination ordering.
The fact that this requires extra classes and the delete (new ...)
silliness is admittedly gross, but the purpose of this was just to see
if there was *some* way to avoid hard coding in explicit knowledge of
the entire class hierarchy and still manage to invoke each applicable
member function exactly once.
Some adjustments would be necessary to deal with things like having a
class which doesn't define a version of _f, but the same basic idea
could be used. Basically, you just have to avoid having that class's
destructor invoke _f.
So anyway, now I've spelled out what I would like to see in a bit more
detail. I think that the first proposal, while less than I would like
to see at least solves the basic modularity problem without adding
much at all to the complexity of the language. If the method
combination ordering of destructors were used, it hardly adds anything
at all. It simply provides additional access a facility already
provided by the language in an extremely specialized form. I hope
that my example demonstrates that.
Adding `combined' to the language would eliminate the need for the
bleacherous aspects of my example. It's biggest drawback, in my
opinion, is that specializations of methods have no control over
whether the `inherited'/`next' methods are invoked the way that they
can with CLOS's call-next-method macro. While, adding something like
call-next-method feature would require more run-time support than some
people will be happy with, it need not impose any run-time overhead on
programs that to not take advantage of it.
--
Rick Busdiecker <rfb@lehman.com> and <rfb@cmu.edu>
Lehman Brothers
3 World Financial Center ``I never did give anybody hell. I just told
New York, NY 10285-0900 the truth and they thought it was hell.
(212) 640-9419 - Harry S. Truman