More On Multiple Inheritance

Hans-Dieter.Dreier@materna.de Hans-Dieter.Dreier@materna.de
Wed, 24 Feb 1999 09:52:51 +0100


--ACK7mUN0Sw8ZjKMC7TQHGBjHMzKSh9Zc
Content-type: text/plain; charset="ISO-8859-1"
Content-Transfer-Encoding: quoted-printable

Matthew Tuck wrote:
>Hans-Dieter Dreier wrote:
>
>> First let me define how I understand the term "MI" here:
>> It means that you declare multiple base classes which are
>> somehow "equal" in the sense that none of the base classes
>> is special compared to the others.
>
>This is how I would prefer it - I think though that there are some
>languages that use order to define which methods shadow over the others.

I would not like such a shadowing based on order - it calls for problems
since a little reordering could have impacts that are hard to see
and wouldn't even be detected by the compiler, what is worse !

>> This is quite kludgey, but it should be possible to achieve
>> the same results as with MI, though use of the common base class
>> instance is more complicated since you got to name the reference
>> explicitly.
>
>Yes, this would be the way it would be done with object composition.  I
>think this is the way it would need to be done it a language with
>object-based inheritance rather than class-based inheritance.

Could you please explain the difference ?

>> For me, the emulated version has the following advantages:
>> 1. It is perfectly clear what happens, especially the fact
>> that the base class instance is really shared.
>> IMO this is better than an implicit assumption.
>> Maybe sometimes you don't want sharing; how do you specify
>> which case you want, using tMI?
>
>I previously said I couldn't see a reason why you wouldn't want them
>shared, but after that read it said that not sharing is sometimes what
>is needed.  I haven't been able to track down any examples yet to see
>whether they would be better modelled with object composition anyway (I
>tend to favour inheritance for is-a relationships and composition for
>is-part-of relationships).

To me, that difference "is-a" vs. "has-a" is not as important as it seems t=
o be to other people. I think it is mainly caused by the fact that common l=
anguages emphasize the difference, but "deep underneath" it is just two sid=
es of the same concept: Grouping "features" (Eiffel speak) together in a sl=
ightly different way.

Maybe we could say that "is-a" is a constant grouping (i.e. participants do=
 not change during the lifetime of the aggregate), but "has-a" is a mutable=
 grouping (i.e. participants may change) ?

As I see it, object composition (or delegation, or whatever term we use for=
 indirect access) allows both "is-a" and "has-a" at a performance penalty, =
where static "dispatch"  just allows "is-a" at a better (runtime) performan=
ce since it is static and immutable, but imposes rules and restrictions due=
 to merging problems.

How the (possibly combined) name space is to be searched in both cases and =
how name clashes and qualification should to be handled has IMO *nothing* t=
o do with the choice between "is-a" and "has-a": It should be handled indep=
endently ("orthogonal" is the word I like for such an issue). Of course, th=
e compiler may choose default strategies dependent on whether it is "is-a" =
or "has-a", if the user does not specify what he wants. =


>Ideally you would define what sort of sharing you would have in the
>inheritance list.  I don't see that it's easy to do though.

But it would make hidden implications explicit, which is crucial to proper =
understanding what happens and that there might be some possible trap invol=
ved which needs careful documentation.

>> ...
>> =

>> That in itself could be regarded as a minor nuisance;
>> maybe more-than-double inheritance is not used very
>> often.
>
>I would say rather that inheritance of more than two classes with state
>is not done too often.  Stateless mixins would not affect the situation
>of object layout.  Although the method lookup table might be affected in
>the same way.

... for virtual items (for statical dispatch there is no problem, as usual)=
.
The v-table problem is certainly present, but it can be solved: The compile=
r silently generates an intermediate class with the same instance layout an=
d the same v-table layout as the original one, but has the this pointer ref=
erence a v-table filled with *different* references. Where the original con=
tents reflects the static situation of the base class without any overridin=
g, the v-table in the intermediate class takes that into account.

(When rereading it, I see that this could not easily be achieved using the =
instance layout shown below. So maybe it is not so good an idea).

IMO if one introduces a language feature (such as MI), it should be univers=
ally applicable and not be ridden with exceptions and rules that restrict i=
ts use and are good for the occasional surprise.

>> ...
>> I don't like this because it is sure to make
>> compiler construction more complicated and
>> incremental compilation less efficient.
>
>Well it would certainly make it more complicated, that's the nature of
>adding optimisations.  The problem of incremental compilation being less
>efficient is certainly present, but that's going to be there if you want
>to do ANY program-wide optimisation.

True, but I'd try to avoid *needing* to do program-wide optimisations in th=
e first place.

As soon as "optimisations" are no longer transparent to the user, that's th=
e point where alarm bells should start ringing to indicate a possible desig=
n problem.

>> This means that once you have downcasted
>> (even implicitly), you can no longer access
>> the original object, using the modified pointer.
>> I can't quite explain why, but I don't feel
>> comfortable with this implication.
>> Maybe this breaks covariance. It certainly
>> breaks this "classOf" operator I proposed
>> in some former posting, which was supposed
>> to fetch the original (i.e. non-casted)
>> class of some object.
>
>I'm not exactly sure what this means.  Do you mean converting a type to
>it's supertype?  If so, normally casting to a supertype would not change
>the pointer.  It is done purely in the type system and matters only at
>compile-time.  The same methods are called, and the same class-of is
>returned.

I don't agree here. What you say is true only as long as statical dispatch =
can be used, which is not the case for any access to an instance item. If t=
he supertype uses a different instance layout, you *need* to change the poi=
nter to be able to use any of that supertype's methods which rely on their =
instance layout. Or the compiler must provide different method implementati=
ons that can use the instance layout of the MI'ed class - but that means ef=
fectively duplicating a class hierarchy (among other disadvantages). Once y=
ou changed to pointer, you cannot access the original object any longer.

It is possible to work aound the problem by using a "back pointer", but tha=
t can hardly be done transparently IMO.

>> P2. Having a pointer not point to the start
>> of an object makes memory management more
>> complicated. In fact, it breaks a central
>> assumption that I deem neccessary for easy
>> and efficient memory management:
>> That the start of the memory block can be
>> determined if only a reference to the object
>> is known.
>
>I would imagine a field would help this be found.  Either a beginning
>pointer, or an offset (slower but allows moving in memory).  Both are of
>course slower than a direct pointer, and may well not be worth the gain.

I contemplated that problem in the mean time and (hopefully) found a satisf=
ying solution to this. I'll post it separately.


>> Internally using the "cMI" approach:
>> ------------------------------------
>> =

>> There is a memory penalty as well as a execution
>> time penalty because pointers must be used
>> ...
>
>Yes, this would be a huge hit.  Smalltalk does this sort of thing for
>method lookups.  It has to since it has dynamic inheritance where the
>relationships can change at run-time.  But C++ on the other hand, has a
>table for a class with each method, inlcuding inherited ones.  It is
>bigger, but a lot faster.  Also, some of the tables could possibly be
>joined (optimised) together.

AFAIK a table is used only for virtual items, not for statical "dispatch" (=
The term "dispatch" really doesn't suit well since it is an ordinary direct=
 function call to a statical adress).

>Basically inheritance is done by methods delegating to other objects. =

>It's all very similar to composition, except that you don't need to
>syntactically traverse long pointer chains to reference an inherited
>method.

I can't see why traverse long pointer chains? I think it can be done like t=
his in almost any case:

this -> reference to v-table
        reference to instance item 1
        ...

If the instance items (e.g. references to "base classes") are constant (alw=
ays the case when MI is intended and no virtuals are involved), the compile=
r can constant-fold them at compile time.

Otherwise there is one indirection per class hierarchy level involved. How =
deep do you want the class hierarchy to grow?

>Embedding inheritance is a similar technique.  You could find
>more about these in "A Theory Of Objects" by Abadi and Cardelli, if you
>can get your hand on it.  There's plenty of research in this area, I
>haven't compiled many references yet though.

I'll try.


--

Regards,

Hans-Dieter Dreier
(Hans-Dieter.Dreier@materna.de)

--

Regards,

Hans-Dieter Dreier
(Hans-Dieter.Dreier@materna.de)=

--ACK7mUN0Sw8ZjKMC7TQHGBjHMzKSh9Zc
Content-type: text/plain; charset="ISO-8859-1"
Content-Transfer-Encoding: quoted-printable

IDENTIFIKATIONSANGABEN:
a14346a.txt IA5 DX-MAIL X.400 User Agent=

--ACK7mUN0Sw8ZjKMC7TQHGBjHMzKSh9Zc--