More On Multiple Inheritance

Hans-Dieter.Dreier@materna.de Hans-Dieter.Dreier@materna.de
Wed, 3 Mar 1999 19:06:51 +0100


--MuatMU3Ky0AtF4SS5Rqds5j5CBCKH93w
Content-type: text/plain; charset="ISO-8859-1"
Content-transfer-encoding: quoted-printable

>>> 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 ?
>
>Sure.  Languages with object-based inheritance, also known as
>prototyping, do not have classes, only objects.  Inheritance is done via
>the cloning (copying) of objects along with the ability the update
>methods (overriding except at run time).

I see. Object-based inheritance does not need the concept of "class", which=
 appeals to me (as all simple, somewhat elegant solutions do). But I wouldn=
't like it, nevertheless, because it should make typing and all related stu=
ff (type checking, coercing...) much harder if not impossible.

>> 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) ?
>
>Then how do you explain languages with dynamic inheritance (can change
>their superobject?) =3D)

IMO this would rather be "has-a" since the grouping would have to change if=
 the superobject changes. But the new superobject must use the same interfa=
ce definition as the old one, so the meaning of method calls does not chang=
e, at least for those methods that are actually used in the subobject. Othe=
rwise it wouldn't make any sense to me. Imagine you requested "k" to be a r=
eference now where it used to be a number: What should that be good for?

Actually, changing an object's class membership or inheritance hierarchy mi=
ght be needed (since that may occur when the program is changed during deve=
lopment, at the user's request), but in many cases there have to be some ma=
nual adjustments to take care of the environment change.

Example: The user decides that an edit field is no longer sufficient in som=
e place and he want to replace that with a combo box (edit field + drop dow=
n list box), which is quite a different class. Here it would be nice if he =
simply could change the object's class and all matching attributes (font, s=
creen position, message handlers...) would migrate. All attributes that hav=
e no equivalent in the new class would turn to comments (so the code is not=
 lost in case it will be needed later on).

>If an item is not dynamically dispatched on it doesn't need to be in the
>table in the first place, certainly.  But to do this you have to make
>sure that this method entry could never be dispatched to.  That means
>that everywhere this implementation could be used, a static dispatch
>could be performed.  It could be that it could only be determined in
>some places, here it can't be eliminated.  This sort of elimination
>would usually require inter-class optimisation I would think.

If we follow the C++ way but have no true MI, inter-class optimisation woul=
d not be needed: You specify something as "non-virtual", and it *is* non-vi=
rtual: Static "dispatch" is used everywhere; the (static) address is known =
as soon as the item in question has been compiled/linked (if it is a class =
item, like a method), or as soon as the instance layout is known (if it is =
an instance item). No inter-class optimisation needed, as far as I can see.

>> The v-table problem is certainly present, but it can be solved:
>> The compiler silently generates an intermediate class with the same
>> instance layout and the same v-table layout as the original one, but has
>> the this pointer reference a v-table filled with *different* references.
>> Where the original contents reflects the static situation of the base
>> class without any overriding, the v-table in the intermediate class take=
s
>> that into account.
>> =

>> (When rereading it, I see that this could not easily be achieved using t=
he
>> instance layout shown below. So maybe it is not so good an idea).
>
>I'm not sure what you mean by this.  Why would you need a static
>dispatch table?

If the pointer to the v-table is contained in the instance layout, the inte=
rmediate class solution sketched above is not possible because it involves =
changing the instance: Its v-table pointer gets a different value. Since th=
ere is a chance that the instance may be shared ("has-a" involves that ther=
e may be many "owners"), this would mean that the instance's semantics (vir=
tual items, that is) would be changed for those other shares as well.

this-> v-table ptr  -> v-table
       instance ptr -> instance items

If the instance layout were like this, the above problem would not appear.
But this would involve another indirection, thus both memory & time overhea=
d.

>>> Well it would certainly make it more complicated, that's the nature of
>>> adding optimisations.  The problem of incremental compilation being les=
s
>>> efficient is certainly present, but that's going to be there if you wan=
t
>>> to do ANY program-wide optimisation.
>> True, but I'd try to avoid *needing* to do program-wide optimisations in
>> the first place.
>
>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 recompil=
ation and global optimisation, where cMI has it explicitly (forcing the use=
r to write the superclass "MI-ready"). Correct? =


>The real problem with your cMI proposal as I see it, is that, although
>it would avoid this, it would lose the fundamental OOP property that you
>could take an reference to an object of class A or B, where B is a
>subclass of A, and dispatch on a method of A.

Why this?
If there is only one method (in A), and there is static dispatch, the compi=
ler will generate a direct call, since it knows that the object is of type =
A or at least derived from A.
If there are two methods (with same signature) in A and B, and there is sta=
tic dispatch, the compiler will generate a direct call to the method in A. =
Since the object (especially the v-table) remains the same, however, virtua=
l calls from there will still perform correctly.
If there is "virtual" dispatch, the compiler will generate an indirect call=
 using v-table lookup.

>Take a look at your original proposal of cMI.  There is no a in B.  For
>this reason, if I had a reference, it's going to be really hard for me
>to find the a not knowing what class is being the reference.

// cMI:

class A {
 int a;
};

class B {
 int b;
 A& ab;  // contains an A <------------------ here is the reference to a in=
 B
// default constructor for standalone use of B
 B(): ab (new A) {};
// constructor for MI use of B
 B(A& a): ab (a) {};
 };

>> As soon as "optimisations" are no longer transparent to the user, that's
>> the point where alarm bells should start ringing to indicate a possible
>> design problem.
>
>Why aren't they transparent?  You have a tradeoff between compilation
>time and run time.  Both take time and are visible, but you can't say
>they affect what the programmer has to do.

To me, "transparent" means that everything that compiled without the optimi=
sation in question will continue to do so with the optimisation applied. Di=
fferent compile/run times & memory consumptions do not make a difference (e=
xcept when they tend towards infinity).
 =

>As far as I know, supertypes will always use the same instance layout -
>the subtype extends the layout.  Method dispatch would be a nightmare
>without it.

Of course.

>I'm not aware of any class-based inheritance languages which allow the
>class of an object to be changed at run-time I think you're referring
>to.

I think that if you apply a cast in C++ to select a base class for some obj=
ect,
then the this pointer *may* change (usually not, but what happens when MI i=
s involved?)

>We don't seem to be communicating here - could you go into more detail?

If you have the following MI hierarchy:

class A {a};
class B {b};
class C: a, b {c};

this could result in an instance layout for C like this:
this-> a
       b
       c.
If you got a pointer to C, then cast that to be a pointer to B, the compile=
r will have to change the this-pointer to point to the start of B because a=
ll the methods of B will expect this:

       a (not accessible)
this-> b
       c (not accessible)

Although the object itself is not changed (I never meant this!), the pointe=
r points to a different location inside the object, rendering the a and c p=
arts inaccessible from within B.

>> 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).
>
>Yes, this is true.  By static dispatch are you referring here to C
>"static"?  I'm not, so I just want to make sure.

Static as opposed to "dynamic": The compiler generates a direct call rather=
 than an indirect one.

"Static" in C unfortunately has the second meaning of "known only inside th=
e current translation unit", which I did not mean here.

>Few compilers do static dispatch optimising I think, and even in those
>that do, there's going to be a lot of methods that can't be statically
>dispatched on all of the time.  C non "virtual" items can always by
>statically dispatched on, but I've never seen a use for that feature.

This greatly surprises me. I always thought that static dispatch is (at lea=
st in C++) the *standard* dispatch mechanism and is used in at least 80% of=
 all cases.
At least this is true in all rpograms I know of. I do not even regard gener=
ating a direct call for static dispatch as an optimisation. If you got only=
 one instance of an item (here: a method), the address of which is known at=
 compile (or link) time, there is no need for indirect access.

Of course, if you look at other programming languages, the situation may be=
 different (e.g. Smalltalk, but you also note their performance).

>>> 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 lik=
e
>> this in almost any case:
>
>I meant syntactically, like you'd have to do with cMI.

You mean at compile time? Well, that doesn't hurt. It simply is the price t=
o pay for not having a single, flat name space.

>> If the instance items (e.g. references to "base classes") are constant
>> (always the case when MI is intended and no virtuals are involved), the
>> compiler can constant-fold them at compile time.
>
>In tMI this would be static dispatch optimising, in cMI a pointer
>traversal optimisation.  I've never actually thought or heard of the
>latter.

References (&) in C++ are an example: They actually are pointers, but you n=
eed not write the * (deref)-operator - it's assumed by the compiler.

>> Otherwise there is one indirection per class hierarchy level involved. H=
ow
>> deep do you want the class hierarchy to grow?
>
>Yes, since this information could be at a constant location.  This
>location would probably need program-wide optimisation to determine
>though!

If the compiler just needed to look up information that was gathered when t=
he base class was compiled, I would not call that "program-wide optimisatio=
n". I imagine it would be stored in some symbol table associated with the b=
ase class and could be looked up there quite fast.

--

Regards,

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

--MuatMU3Ky0AtF4SS5Rqds5j5CBCKH93w
Content-type: text/plain; charset="ISO-8859-1"
Content-transfer-encoding: quoted-printable

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

--MuatMU3Ky0AtF4SS5Rqds5j5CBCKH93w--