From: maxtal@extro.ucc.su.OZ.AU (John MAX Skaller)
Newsgroups: comp.object
Subject: Re: O.M(...) vs M(...), and is the Real World O-O?
Date: 27 Sep 92 14:09:32 GMT
Organization: MAXTAL P/L C/- University Computing Centre, Sydney

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

	Perhaps you misunderstood. Perhaps I mistated it.
With downcasting, the function f examines its parameter and determines
that it is in fact a Derived object. Ok?

	Then it calls 'set', which is a public member of Derived. Ok?
And that changes i. Ok?

	But i is a protected member of Base, the declared type of the object.
Agree?

	Thus, downcasting allows the protected interface of
the *declared* type of an object to be violated. (Read 'static'
instead of declared if you want to)

	I believe this is beyond dispute. What we can argue
about is whether it matters to violate protected interface of
the declared type provided we dont violate the interface
of the actual type.

	I think it matters.
>
>To take the example further for a real attempt at violation, lets say class
>Derived makes protected base member i a declaration specifier and increases
>its access to public.  Now f can take a Base reference or pointer (not B and
>no slicing) and try checked downcasting.  If it succeeds, the access is Ok
>since Derived made the protected base member public.  

	Well, here is our disagreement. I dont think it should
be possible to access an object on anything but its declared type.


>If the downcast fails,
>all is well because f won't be able to access a Base's protected members.  In
>other words, all is well because downcasting obviously enforces interface
>access specifications.

	I do not agree that it is ok to access the interface
of an object based on its actual type. The correct method for accessing
members based on the run-time type of an object is by virtual functions.

>
>And with unchecked downcasting, as pointed out above, f could never access
>Base's protected members.
>
	Checking has to do with safety. The issue here is soundness.
The whole derived interface is hidden when an object is accessed as
a base, and to my way of thinking 'hidden' is even more private
and inviolate that 'private'.

	When I create and abstract base class, and then write routines
which manipulate that base class, I expect those routines to be
able to manipulate the objects via the abstract interface only.
A routine so written will work for ANY derived class (provide that
derived class has the proper semantics), and that is the basis
of polymorphism.

>>
>>>Checked downcasting is like
>>>a typecase statement, check the type and then access it in a statically
>>>checked (efficient) way.  This is good when a method can do better in certain
>>>cases and with certain kinds of objects.  
>>
>>	I dont follow. A call to a virtual function is certainly more
>>efficient than a manually coded type check followed by a static call.
>
>No, its not (in general) any more efficient.  After one check with
>downcasting all subsequent calls are static and direct and not thru a
>function pointer.  

	OK, yes, this is true.

>And using virtuals assumes all functions of all derived
>classes are moved into the base class.  This is most undesirable with the
>most obvious reason being an unnecessary coupling of Base and Derived
>classes (and there are many more).

	If you get this problem (fat interface) you have a lousy design.
And you're still thinking in terms of single inheritance. Anyone
that tries to emulate Smalltalks Set, Bag etc classes based on Object
is using the wrong language. This is absolutely the wrong thing
to do in C++.

	The right thing to do in C++ is to use multiple inheritance.
>
>>	Downcasting is usually used when the Derived class is NOT a subtype
>>of the Base class, one downcasts to get access to some function Dmem()
>>which is NOT a member of the base class.
>
>Not at all.  For example, a device class can be declared.  Certain kinds of
>devices may have certain operations and certain kinds may not.  Downcasting
>is *one* way of accessing these derived operations without moving all derived
>class functions into the base class as virtuals.  Another solution is to
>dynamically query if a message is available in a polymorphic object and only
>invoke it if it is; this is common in Objective-C.

	And BOTH of these would be bad design for C++ in my books.
In C++ you have to know the interface you intend to use statically.
Only the implementation is allowed to be dynamic (via virtual
functions).

	If you have, for example a base class 'stream' device driver,
and then you have 'serialport' and 'diskdrive' then it is true
that for example disk files can be 'rewound' to the start,
whereas serial ports can have their baud rate changed.

	If you write a routine using 'stream' it must not depend
on the specific device type. ONLY on the attributes of all
streams. That is called device independence.

	Suppose you downcast to 'serialport' or 'diskdrive',
so as to, say open the stream correctly. Thats ok in
your books isnt it?

	But you just wiped device independence. Because now
I add 'video' to the devices, and your routine fails.
In by books, sticking to the declared interface
guarrantees polymorphism.
>
>>>While messaging is usually Ok
>>>it doesn't cover these idiosyncratic cases.  
>>
>>	In other words you want to have a large set of derived classes,
>>most of which are subtypes, but a few are not quite subtypes, so you
>>put special tests in for these few cases. IMHO there must be a better
>>way, for example, tagged pointers.
>
>Not at all.  In the device example there are many kinds of devices and
>they do not all share the same operations.  And tagged pointers
>can be used to implement downcasting, at least by my use of the term.

	Tagged pointers allow SELECTION. From a statically
determined set. That set must be declared ***statically*** in advance.
Downcasting allows checking on any type.
Tagged pointers are completely statically type safe and sound.
Checked downcasting is safe, but not IMHO sound.

	Tagged pointers are used to implement heteromorphism,
which is the opposite of polymorphism. Hetermorphism manages
similar looking interfaces compactly, polymorphism requires identical
interfaces.
>
>>>Not being able to access
>>>derived class interfaces is too severe a restriction; I think, and really
>>>hurts for the most general cases too.  
>>
>>	This is a value judgement about a trade off, and you might well
>>be right.
>
>Not really.  In (dynamically typed) Smalltalk you can take any object and
>pass it any message; this is lost with only static typing (types only
>available at compile time) or is at least a restricted mess.

	So you lose power. Smalltalk dynamism is powerful. I agree.
It is also very hard to catch errors at compile time. So
its hard to be sure your code works. C++ strong typing removes
from existance a whole class of errors (type errors) at compile time.
I do not see how you fail to perceive this as a trade off of
power against correctness. Given a certain task, one has to
chose which is more important, to decide to use Smalltalk or
to use C++ for that job. Thats a value judgement.

>
>>	class Q {
>>	public:
>>		void enQ(X&);
>>		X deQ();
>>		int nQ()const;
>>	};
>>
>>A queue manager routine has calls
>>
>>	getsome(Q&); .... putsome(Q&);
>>
>>where 'getsome' and 'putsome' are user supplied source and sink routiines.
>>Now the Q manager is supposed to preserve the order of things in the queue.
>>But as it stands, getsome or putsome might scramble the order. So we
>>change the Q class---
>>
>>	class Qbase { public: int nQ()const ...}
>>	class NQ : public virtual Qbase { public: enQ(X&); };
>>	class DQ : public virtual Qbase { public: X deQ(); };
>>	class Q : public virtual NQ, public virtual DQ {};
>>
>>Now I can declare
>>
>>	getsome(NQ&);
>>	putsome(DQ&);
>>
>>and, without downcasting these routines cannot scramble the Q.
>>With downcasting, they can.
>
>I'm sorry, but I don't see this.  

	I dont see how you dont see it :-)

	I'm not sure if restating the obvious (to me) will help.

Without downcasting:

	the routine getsome(NQ&) cannot access deQ,
	because its not in the interface of NQ
	and also
	because it cant cast NQ to Q and thus to DQ.
	Thus it cannot change the order of the objects already
	in the queue.

With downcasting

	the routine getsome(NQ&) can discover the object is a Q
	and then cast from NQ to Q and thus gain access to deQ
	Now it can both deQ and enQ objects, so it can
	arbitrarily reorder the Q.


>Where are these routines declared; in a
>class? 

	Does it really matter?

> Does your point still hold with the new given C++ rules?

	There are no 'new given C++ rules' --- yet :-)

>
>bob
>objsys@netcom.com


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