More On Multiple Inheritance

Matthew Tuck matty@box.net.au
Sat, 13 Mar 1999 20:06:24 +1030


Hans-Dieter.Dreier@materna.de wrote:

> Types correspond to the interface and classes correspond to the
> implementation?  That would mean that all objects of a given type must
> conform to the same interface, right?

Well there are no "classes", just initial objects that are cloned, but
yes, all objects can still be made to conform to a typing hierachy.
 
>> Agreed.  It would be interesting to be able to play around with things
>> at run-time using the environment, but things like changing types have
>> to be handled properly.
> If the editor and the source are both objects, with one of them acting
> on the other, anytime is runtime for some part of the system. For some
> objects a compiled version exists (and thus they are executable).
> Others consist only of source code and a notice that says "I need to be
> compiled before use". Still other objects cannot be compiled (a stack,
> for example).

What would you say the ramifications of this point of view are?

<snip - separate message>

>> This is one reason why I try not use the word class anymore except when
>> using the traditional connotation of a unified type and implementation.
>> It can confuse the situation.  Some people and languages have tried to
>> use class to refer to impl only, but I don't think that's a good idea,
>> since it's confusing.

I admit this criticism can be levelled against 'type' too, since it was
used to represent combined type/impls in non-OOPLs.  I think that the
word better represents the idea of an interface than class, plus it is
less entrenched now OOPs are common.  Concepts like "strong typing",
"type systems", etc. really only refer to the interface.  But it is a
bit arbitrary.
 
> Good idea. Maybe you want to give your _definition_ of type, impl and
> signature, especially where one begins and the other ends.

Well a type defines the interface.  Signature is a word I usually use
for methods but I'd say it's the same as interface.  Perhaps the only
different between type and interface/signature is that type gives an
interface and name, and if you have name typing rather than structural
typing, two interfaces can be the same yet have a different type.

An implementation is the code that obeys a type's interface.  They are
semantically and syntactically different entities.

> For
> example, if the compiler detects some property of a function (e.g.
> "has no side effects"), but the user did not _demand_ that, does this
> belong to type?

Essentially an impl will always have the same interface as it's type,
there's no point it having more since it can never be used.  The type
embodies all the interface definition.

Deriving things like no side effects would require looking at a piece of
code in an impl.  For a start there might be more than one impl to look
at.  Not an insurmountable problem.

But then you might want to subtype the type.  If it was typed as
side-effectless, a subtype would be forced to obey this, which might not
be desired.

Furthermore, the user might want to change this later.  There are three
options I can see as to how the compiler/editor would handle the
situation.

(a) Don't assume the keyword and compile OK.
(b) Add in the typing keyword.
(c) Emit a warning and compile as if no side effects were assumed.

I don't like (b) since it goes behind the users back and requires
changing it later.  Additionally parts of the program might be written
around this assumption (we require no side effects to be used here, eg
in assertions), then break later when the programmer changes it back. 
It might not even be the same programmer writing and supplying the type
& impl, making it even more likely for this to happen.

(c) also suffers the last problem.

So the only solution left is (a).  If the writer knows it was a
side-effectless function and uses it a place where you expect so side
effects, you could again include an "Add 'side-effect' keyword to type
declaration" option to fix it, although you'd have to be wary since you
might not be responsible for the type.

Which brings up the question about possible access control in the
editor.  This would probably be on a per module basis - every module
would have certain people allowed to change it.  Hence if you weren't
alllowed to change the type, the solution above would appear on the menu
greyed out.

> If one implementation uses a get call where the other
> allows direct (read) access, do they have different type? Or different
> signature?

Rather than working from impl to type I work the other way around (since
impls can be added to the system), but the basic answer is the are the
same.

If the type dictates read-write the impl must provide a get/set or a
var.  Or just a get or a var for a read-only, and similar for
write-only.

Since whether the impl uses get/sets or a variable is transparent to the
client, it is not a part of the type, since the type constrains subtypes
and implementations to its interface.
 
>> Well if you want to define non-overridable methods then yeah, you can,
>> but this defeats the spirit of OOP.  You should always have the
>> opportunity to override.  I have never come across a use of non virtual
>> methods other than as a kludge to avoid dynamic dispatch.  Flexibility,
>> runtime-efficiency, compile-time efficiency, choose two.
> To allow non-overrideables comes really cheap and has no adverse
> performance impact. And it enhances expressiveness by adding another
> orthogonal facility (whether that is needed often is another question).
> So why not implement it?

Well I'll explain a bit more about that in a second, but I will point
out now that there are actually some simple static dispatch situations. 
If you can find a type A with only one impl z then you can always static
dispatch to it.  This includes allowing an unlimited number of subtypes
of A as long as for A and its subtypes there's only one impl that
exists.  This might be an impl of A or any of its subtypes.

Admittedly, again this is usually an inter-class optimisation thing
since you can't guarantee there won't be a subtype/impl unless you know
all the code that can be loaded.  And runtime loading/generation makes
it even harder.

> If the class author wants to make sure that some service is called
> from a particular method, how can he enforce that otherwise? Even if
> the overriding function sticks to the contract, the internal
> _semantics_ cannot be checked by the compiler in such detail.

C-style non-virtual does not enforce this - if you convert the pointer
to a subtype and dispatch you still get different code.  What's the deal
with that?  The only thing that can do this is a Java-style final which
does prevent overrides.  But this has its own problems.

>>>> That's true.  You couldn't do MI with the scheme with making it
>>>> fragile.  Although I don't see cMI is any different.
>>> Both schemes have some fragility. tMI has it implicitly, requiring
>>> recompilation and global optimisation, where cMI has it explicitly
>>> (forcing the user to write the superclass "MI-ready"). Correct?
>> Yes, but I was talking about your language level true MI using
>> composition as the implementation.
> Pardon? That's what I meant by cMI above. That it requires to write
> the superclass "MI-ready".

I got the impression before that you were discussing an implementation
of true MI in the language which gets converted to cMI, i.e. an object
composed of other objects, rather than one big object.

>> Yes but normally the B will physically contain an a, hence no matter
>> whether you have an A or B, you can look inside the object to find the
>> a.  With composition, you don't know this.  If it's impl A, you can,
>> if impl B, you have to follow a pointer.  Then problem is that you're
>> no longer extending the layout.
> IMO B contains an A rather than an a (which is an int, BTW, we must
> not confuse types and names here). That's what the user specifies
> using cMI, and that's what he gets. If the member ab (which we're
> talking about) were specified as transparent, he could omit the
> qualification "ab.". At the source level it would look like tMI and
> the compiler would take care of the derefencing (from the
> qualification) automagically. The code would (maybe) be different,
> yes, but who cares?

I understand the names could be transparent, when I was referring to the
_generated code_ rather than the source.  The generated source would no
longer involve a method dispatch, but would have to involve an if
statement that looked at the type tag to determine the layout and where
to go.

If classes were added to the system at runtime, this wouldn't work
anymore  (although, come to think of it, neither might MI layout
algorithms ...)
 
>> You could generate if/then code for each access, but that's going to
>> cause very bloated code.
>> If you had get/sets for all fields, you could direct it to the correct
>> place, but this introduces a dynamic dispatch where there previously
>> would not have been one.
> I don't see this.

Again, at the generated code level.

> Well there's the old (maybe far-fetched) example of the superclass
> that can't be recompiled. Maybe we choose to put stuff (superclasses)
> into DLLs. Those DLLs would be shared by many applications. Do you
> really want to have different DLLs with different instance layouts
> because some users decided to change their code?

Yes that's true.  Under this system you would essentially need the app
to recompile to work with a new version of the DLL with new layout. 
However I think this wouldn't be much different to how it now works with
(say) VBRUN.  They probably wouldn't allow changing type layouts either!

So this being said, knowing we need some sort of fixed layout, if we
wanted to improve the situation without a recompile of the DLL client we
would probably need to put in a level of indirection like the offset
table I proposed.

> Maybe this seems far-fetched, but if I look at the stuff I', working
> with at the moment (Microsoft's ATL mixed with STL and own classes),
> I can tell you: Inheritance relationships there are a nightmare. Many
> classes have lots of base classes, all of them templatized of course
> (and mostly pure virtual).

How would you improve the inheritance hierachy?  What sort of
superfluous parent classes exist?  I've always found using a few mixins
makes things very flexible if a little harder to visualise.

>> I don't think the pointer would change, just the typing aspects.  If
>> you have a fixed layout the pointer wouldn't need to change.
> I wanted to know for sure, so I tried. VC++ 6.0 definitely changes the
> pointer if you cast to a base class:

Interesting.  C++ has no GC usually, so that problem does not apply to
it, although what if you tried to deallocate the B* ?. 

I don't know whether C++ has RTTI yet, although I heard it might be
getting it, so don't know if that's a problem.

And it looks like it doesn't care about object identity comparisons,
which coming from C, does not suprise me.

> How could it be otherwise? If you can show an example, I'd be very
> interested to learn about it.

Well I was referring to algorithms where the layout is fixed at compile
time.  Basically each mixin gets a fixed position it always sits at, so
you can find the mixin from an object pointer no matter its type.  You
possibly get holes, which could be rather big.

A layout table for each mixin for each impl might work to avoid this,
similar to a dispatch table.  In the same way, obvious efficiency
disadvantages apply, but can be optimised.
 
> Maybe it also depends on personal style. If I write a class without
> "virtual", I can be pretty sure that noone unintentionally breaks my
> code by overridding with an inappropriate replacement. It's sort of
> encapsulation.

This is purely due to the absence of multiple implementations.  Hence
you can't write another implementation which uses delegation objects
instead and gets around those problems.  So it's kind of encapsulation
through language strangulation.

But anyway, one option would be to put a "final" on types (and you can
only have one impl), which would avoid getting around it via
delegation.  This is essentially moving the Java solution into the
multi-impl world.  However, I don't like this at all, I've encountered
final classes I really want to override before.

Often what the author really wants is to restrict overriding of their
implementation.  Hence they won't care if you write a whole new
implementation.

Other sorts of override modifiers are possible as well - prefix only,
suffix only, surround only, etc.  CLOS does something like this with its
overriding.

So in the end, you can still set up another implementation which uses a
delegate to get around not being able to override.  Maybe this is OK.

Perhaps the best solution though is to try and work out superior ways of
handling the situation, by modifying the code and possibly enhancing the
language.

Can you think of any examples where you've wanted to use final?

The situation I've discovered is you have certain methods, but you only
want to call them in a certain way, order, etc, so you make the driver
method final.

One language enhancement that would therefore be quite useful would be
the ability to override a method, but not to be able to call it. 
Abstract methods could be filled in quite fine by the subimpl, but the
only way you could get to run those methods would be through actually
calling the driver method.

Although you probably want to leave final on the impl just to show it
should be run, this would stop delegation in its tracks in this
situation.

In fact, the "its either private or totally public" stance of most
languages these days is one of my dear issues, but that's another story.
 
> I really don't see the problem. If you want it virtual, you hit "Go to
> definition"  and insert the word "virtual". Nothing else needs to
> change. If the editor is user-friendly, it's a matter of about 5 mouse
> clicks and 7 or 8 keystrokes.

The problem is the parent impl and subimpl might not be written by the
same person.  The parent impl might be obfuscated, or beyond your
access.  And there is often no reason for the keyword to be there in the
first place.  If you shouldn't be overriding the method, ok, but it can
often be done just for efficiency.  Non-overridable methods are the
exception rather than the rule in my experience.

> IMO the same thing holds true for a cMI approach. Because the task
> is clear and the language has a feature ("transparent") which makes it
> transparent to the source code, it can easily be done by the compiler.
> Maybe you got some more options to choose among (Repeated inheritance
> resolution, for example). If the compiler _asks_, at least the
> potential problem of rep.inher. is communicated to the programmer.

Well essentially stopping with errors is the compiler's way of asking. 
The programmer should know the best way to fix the program - the
compiler might well not.

> IMO current compilers are not interactive enough. That's perfectly
> understandable; noone wants to be asked the same questions over and
> over again for each compilation because the compiler is not allowed to
> change the source (to record that this warning has already been issued,
> or example).

Do you think there's anything that could be done better than my way of
handling this (general problem)?  That is, issue warnings or errors to
the user and let them right-click on the error to select a solution.  It
should be easier to solve since the errors are centralised in the errors
view rather than throughout the program.  I've tried to make it as easy
as possible without actually making the compiler change the program.

> Oh I see. Maybe you should call that "coding time" to avoid confusion
> with types (the interface thing). Well, that's exactly what
> transparent names are for: To relieve you from typing the extra
> qualification. If you declare Object2 as transparent, you may write
> either version. Of course there must not be an ambiguity. Object may
> not contain a name "Method", for example. Eiffel uses renaming in such
> cases.

Yes, I'm pretty sure I said that it was similar to transparent names in
a previous post.  The only difference is that you can update methods as
if they were fields.
 
> Sure, but if it occurs at compile time and is resolved then (constant
> folding), who cares?

All I'm really disputing the use of "constant folding".  My understand
of the term was it was taking a function with constant parameters and
working out a constant result.  Field access could be seen as this.  But
simply knowing the location at compile-time does not come under that
definition, although you could still bring it under the heading of
"constant determination" or whatever term you want to dream up.  But
maybe I'm wrong ...

-- 
     Matthew Tuck - Software Developer & All-Round Nice Guy
             mailto:matty@box.net.au (ICQ #8125618)
       Check out the Ultra programming language project!
              http://www.box.net.au/~matty/ultra/