From: maxtal@extro.ucc.su.OZ.AU (John MAX Skaller)
Subject: Re: O.M(...) vs M(...), and is the Real World O-O?
Organization: MAXTAL P/L C/- University Computing Centre, Sydney
Date: Sun, 27 Sep 1992 15:37:18 GMT

In article <xa#p5wk.objsys@netcom.com> Bob Hathaway <objsys@netcom.com> writes:
>In article <7+#prtg.objsys@netcom.com> Bob Hathaway <objsys@netcom.com> writes:
>>In article <1992Sep23.084548.14002@ucc.su.OZ.AU> maxtal@extro.ucc.su.OZ.AU (John MAX Skaller) writes:
>>>	class Base {
>>>	protected: int i;
>>>	public: int get()const {return i;}
>>>	};
>>>
>>>	class Derived : public Base {
>>>	public: void set(int j){i=j;}
>>>	};
>>>
>>>	f(B&);
>>>
>>>Without downcasting, f cannot change the object. With downcasting,
>>>and assuming the argument was really a Derived, then f can change
>>>the object, that is, f can violate the protected interface of
>>>the static type of its argument.
>>
>>I'm sorry, but you're mistaken.  f is not a friend and can only access public
>>members of Base or Derived.  You're mistaken about outside access to
>>protected base members - this is not allowed in C++.
>
>Opps, I just noticed the semantics of Derived::set.  Since Derived objects
>are supposed to be able to modify i this is Ok.  And Base desires this
>or i wouldn't be protected.  There is nothing amiss here.  

	In your opinion. In my opinion, the *might* be something
amiss here. The reasoning is that, without downcasting, without knowing
anything about f(Base&) other than it declaration, I can assume
if I pass f(Base&) and object, it will remain unchanged. 
I deduce this from the fact that there is no way that a Base object
can be changed via the Base interface, and there is no
way to go from the Base interface to any derived interface.
Thus, I can make the assumption that the object I pass is
constant.

	I can use this knowledge to my advantage.
For example, this knowledge has obvious applications to optimisation.
But more importantly, if it is critical that the object not
be changed then I can call f in confidence. Compiler guarranteed
confidence.

	With downcasting, I cannot make any assumptions without
knowing more about the routine f. I might have to examine
the code. That might be impossible---f might not have been written
yet!

	Now I definitely lose that optimisation advantage. 
Worse, if it is critical that the object not change, then
I cannot call f(Base&) at all. 

	I'm sure you can imagine a scenario where a mission
critical program is in operation, and the programmer
has to add a reporting routine f(Base&) to print the value
of i. Without downcasting the call to f can be added
and the programmer has reasonable confidence that the
space shuttle will not explode due to the change.

	With downcasting, I would refuse to make this
change, since it might endanger someone life. (Unless
I wrote f as a local routine myself, perhaps, and
even then it would be a risk :-)
>
>I showed why a strict base interface is usually undesirable to unworkable;

	I dont think you did. Instead, you made some assumptions about
how to write code, and then showed that given your assumptions
downcasting is necessary to avoid certain undesirable conditions.

	But I dont necessarily agree with your assumptions,
so I cant agree with your conclusion. For example,
you say that strict conformance to the declared type leads
to a fat interface in the base type.

	If a whole lot of your derived types are different,
fine, manage each one separately. But a routine accessing
a Base should only access the common features, thats the
whole point of polymorphism. If you want to access the
special features of Derived, write a routine that does
that. If you dont know what type an object is, and you 
need to, you stuffed up the design! You shouldn't have
forgotten :-)

>but perhaps you have a point, that this forced strict conformance could be
>available if desired.  

	The shoe is on the other foot. The forced strict
conformance is what we have in C++ now. It should be the default!
It is more a question of allowing non-strict conformance sometimes
if desired. I call this Contstrained Downcasting. But I dont
know what reasonable constraints should be.


>Can you provide a case?

	I provided two examples. Perhaps not good ones. 
You stated that because 'i' was protected the base class intended
its deriving classes to modify i and this is so. But it also
intended its public users to not modify i, which is why i is not
made public.

	Clearly, f is a public user of Base, yet it is able to 
modify i for some objects if they happen to be of one particular
exact type, namely Derived. Perhaps for others too, 
there is no way to know which ones  without examining the source
of f or every class Derived from Base.

	IMHO downcasting is a facility for heteromorphism,
not polymorphism, and it is very suspect one. It introduces
a limited form of dynamism to C++ to make it a bit more
like Smalltalk in the worst possible ways.

	It fails to be anywhere near as powerful as Smalltalks
ability to call any object with any message. And yet it
might open up the type system to violations that in Smalltalk
are acceptable because the full dynamism is so poweful,
but in C++ the advantages are small compared to the
disadvantages.

	For example, to manage heteromorphic containers,
one can use tagged pointers:

	[A,B,C] *p; // p points to an A, B, or C

These are completely typesafe and sound, they are
checked at compile time. And no common base is needed.
The implementation is trivial and fast.
Tagged pointers are a generic, selection facility,
like case statements. They have no relationship
to inheritance or polymorphism at all.

There is only one issue, apart from this issue,
tagged pointers for heterogeneity are so much better
than downcasting that there cannot possibly be any 
question which way to go.

The issue is extensibility. To extend a tagged pointer
you have to modify the source code and recompile.
Quite a lot of code---including routines already locked
away in libraries--- might have to be recompiled.

Downcasting does not suffer from this problem.
You can happily downcast in your routines,
and pass the objects around to those who do not
know about the new type you derived.
The other routines do not have to be recompiled.

There are other uses for downcasting, but 
the case of collections of objects of types
drawn from a finite set is not one of them.

Downcasting is never required in domestic programs,
that is, those which are statically linked,
create all the objects, do something, destroy
everything, and then terminate. 

-- 
;----------------------------------------------------------------------
        JOHN (MAX) SKALLER,         maxtal@extro.ucc.su.oz.au
	Maxtal Pty Ltd, 6 MacKay St ASHFIELD, NSW 2131, AUSTRALIA
;--------------- SCIENTIFIC AND ENGINEERING SOFTWARE ------------------
Newsgroups: comp.object