More On Multiple Inheritance

Hans-Dieter.Dreier@materna.de Hans-Dieter.Dreier@materna.de
Tue, 9 Mar 1999 16:44:08 +0100


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

>Hans-Dieter.Dreier@materna.de wrote:
>
>>> Sure.  Languages with object-based inheritance, also known as
>>> prototyping, do not have classes, only objects.  Inheritance is done vi=
a
>>> 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 stuff (type checking, coercing...) much harder if not
>> impossible.
>
>Not as far as I can see.  Types can still stay the same, it's just that
>there are no fixed implementations.

Types correspond to the interface and classes correspond to the implementat=
ion?
That would mean that all objects of a given type must conform to the same i=
nterface, right?

>> Actually, changing an object's class membership or inheritance hierarchy
>> might be needed (since that may occur when the program is changed during
>> development, at the user's request), but in many cases there have to be
>> some manual adjustments to take care of the environment change.
>
>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 t=
he other, anytime is runtime for some part of the system. For some objects =
a compiled version exists (and thus they are executable). Others consist on=
ly 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).

>> Example: The user decides that an edit field is no longer sufficient
>> in some place and he want to replace that with a combo box (edit field +
>> drop down 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, screen position, message handlers...) would migrate.
>> All attributes that have no equivalent in the new class would turn to
>> comments (so the code is not lost in case it will be needed later on).
>
>Most GUI's expect "component" type and don't care what subtype or
>implementation is, so you should be able to change this at runtime.  The
>main problem with this, and dynamic inheritance, is ensuring state is
>preserved.  With public state this is not too bad - private state is
>much harder - but if there's sufficient interface for the purposes of
>universal conversion, it should be possible.

Just having one "component" type may not provide the versaility that is nec=
essary.
The problem is not that hard.
At edit time usually templates are changed rather than "live" objects.
Private state is normally only used at runtime (when the object is "live").

Explanation: The term "template" is used here not in the C++ sense. It basi=
cally is a constructor call with some parameters already attached (editable=
 in an outline). It resides as a member variable declaration inside some ob=
ject and will be called as soon as that object is created. For example, a w=
indow may contain controls. Each control creation is done by declaring a co=
ntrol as a member variable of the window. The  editor supplies the outline =
for entering the constructor's parameters. Which parameters to use is deter=
mined by looking at the instance items' declarations of the control. If the=
y are tagged as "properties", they are included. Some properties may be inh=
erited from the parent (not the base class, but the window, in this case). =
An example of property inheritance might be a font specification, or the ba=
ckground color. This allows to easily change them by just changing the prop=
erty value of the parent. Look at Centura (or maybe at Delphi, but I'm not =
sure whether they support property inheritance).

I already mentioned this in an (early) posting last year.

There are other (similar) ways to achive the same result. Instead of taggin=
g the instance variables, one could specify the constructor to use along wi=
th the declaration. This would allow for multiple versions of templates and=
 would also have the advantage of not introducing yet another keyword.

>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.

Good idea. Maybe you want to give your _definition_ of type, impl and signa=
ture, especially where one begins and the other ends. For example, if the c=
ompiler detects some property of a function (e.g. "has no side effects"), b=
ut the user did not _demand_ that, does this belong to type? If one impleme=
ntation uses a get call where the other allows direct (read) access, do the=
y have different type? Or different signature?

>> If we follow the C++ way but have no true MI, inter-class optimisation
>> would not be needed: You specify something as "non-virtual", and it *is*
>> non-virtual: 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.
>
>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 performanc=
e impact. And it enhances expressiveness by adding another orthogonal facil=
ity (whether that is needed often is another question). So why not implemen=
t it?

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

>>> 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 sup=
erclass "MI-ready".

>> 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) {};
>>  };
>
>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 con=
fuse types and names here). That's what the user specifies using cMI, and t=
hat's what he gets. If the member ab (which we're talking about) were speci=
fied as transparent, he could omit the qualification "ab.". At the source l=
evel it would look like tMI and the compiler would take care of the derefen=
cing (from the qualification) automagically. The code would (maybe) be diff=
erent, yes, but who cares?

>You could generate if/then code for each access, but that's going to
>cause very bloated code.

I don't see this.

>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.
>
>>> 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
>> optimisation in question will continue to do so with the optimisation
>> applied. Different compile/run times & memory consumptions do not
>> make a difference (except when they tend towards infinity).
>
>Sure.  How is it not transparent in this situation?

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. Th=
ose DLLs would be shared by many applications. Do you really want to have d=
ifferent DLLs with different instance layouts because some users decided to=
 change their code?
Remember: When application AppA is compiled, there is no way to know what A=
ppB needs inheritance-wise.
If the compiler has to tell you: Sorry, you can't do this, I'd call that no=
n-transparent since it affects the user.

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

>> I think that if you apply a cast in C++ to select a base class for some
>> object, then the this pointer *may* change (usually not, but what happen=
s
>> when MI is involved?)
>
>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 poin=
ter if you cast to a base class:

class A  {
public:
	int a;
	A();
	virtual ~A();
};

class B  {
public:
	int b;
	B();
	virtual ~B();
};

class D : =

	public A, =

	public B  {
public:
	int d;
	D();
	virtual ~D();
};

int main(int argc, char* argv[]){
	D* d =3D new D;
	printf("%lx %lx %lx\n", (long) d, (long) (A*) d, (long) (B*) d);
	return 0;
}

(ctors & dtors omitted for brevity)

This prints "430210 430210 430218" on my computer. The first two are identi=
cal because D starts with A.
How could it be otherwise? If you can show an example, I'd be very interest=
ed to learn about it.

>>> 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
>> least in C++) the *standard* dispatch mechanism and is used in at least
>> 80% of all cases.
>
>Well I guess it depends on the way you program.  A lot of C++ uses
>legacy C and hence it is static dispatch.  When I've written in C++,
>I've always written virtual or pure virtual calls.

Maybe it also depends on personal style. If I write a class without "virtua=
l", I can be pretty sure that noone unintentionally breaks my code by overr=
idding with an inappropriate replacement. It's sort of encapsulation.

>If efficiency is that important I can change it later, just as I'd like
>a language where if efficiency was that important I could optimise it
>later.  Except I wouldn't need to keep writing "virtual" everywhere
>(although it's possible to make virtual default and nonvirtual an
>override).  Java is similar with "final", except you can't override it
>at all (I like this facility, but on implementation inheritance, not
>type inheritance).

I really don't see the problem. If you want it virtual, you hit "Go to defi=
nition"  and insert the word "virtual". Nothing else needs to change. If th=
e editor is user-friendly, it's a matter of about 5 mouse clicks and 7 or 8=
 keystrokes.

The compiler can also issue a warning that you are overriding a function th=
at is not declared virtual and ask you "Do you want to make the member virt=
ual". Then it boils down to just one mouse click.

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 so=
me more options to choose among (Repeated inheritance resolution, for examp=
le). If the compiler _asks_, at least the potential problem of rep.inher. i=
s communicated to the programmer.

IMO current compilers are not interactive enough. That's perfectly understa=
ndable; 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, for example).

>>> 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 pric=
e
>to pay for not having a single, flat name space.
>
>No I meant at type time, so instead of typing Object.Object2.Method you
>type Object.Method.

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 ar=
e for: To relieve you from typing the extra qualification. If you declare O=
bject2 as transparent, you may write either version. Of course there must n=
ot be an ambiguity. Object may not contain a name "Method", for example. Ei=
ffel uses renaming in such cases. =


>> References (&) in C++ are an example: They actually are pointers,
>> but you need not write the * (deref)-operator - it's assumed by the
>> compiler.
>
>This is not really the same.  A pointer traversal optimisation would be
>statically determining a memory pointer that is potentially many levels
>deep.

Sure, but if it occurs at compile time and is resolved then (constant foldi=
ng), who cares?


--

Regards,

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

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

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

--QR3jVduJjEIIyuc6YlR3O2SkJsH7Bg3v--