rpj@redcloud.cad.mcc.com (Rich Johns) (07/28/89)
Assume a situation like the following:
class Window {
public:
virtual Window* Copy();
// other stuff
};
class BorderedWindow : public Window {
public:
BorderedWindow* Copy();
// other stuff
};
class ScrollableWindow : BorderedWindow {
public:
ScrollableWindow* Copy();
// other stuff
};
I would like to be able to change the return type of the virtual
Copy(). G++ 1.35.1 rejects this, and I assume cfront would do the
same. I have only recently upgraded to g++ 1.35.1, and was previously
using 1.32 which allowed me to do this. Could someone explain why this
is not allowed. It seems like the compiler could easily determine
whether the change in return type were reasonable or dangerous. That
is to say, if the changed return type is derived from the return type
in the base class, let it be.
I'm trying to avoid, what seems to me to be, unecessary casting. If
BorderedWindow and ScrollableWindow each have methods their base
classes do not, then I lose the ability to deal with all of them
generically. For example, only BorderedWindows have a borderWidth, and
only scrollable windows have a ScrollUp() method. What I could do is
define ScrollUp() as a virtual on Window with a dummy definition. This
is alright I guess, although not really intuitive because a plain
window does not scroll. I could also put boderWidth variable on
Window, but this seems even less intuitive. Nonetheless, I would do it
if not for the fact that object Window is part of a library, which I
can't alter. (It just so happens that in this case I could modifiy the
library, but I don't want to do this. I want to be able to pick up new
versions of the library and not have to worry about always
retrofitting my own personal hacks).
I could also establish a new base clas called MyWindow, derived from
Window. But would'nt I then have to rename Copy() to MyCopy() in order
to change its return type? The only solution seems to be messy casting.
Am I thinking about this in the right way?
Rich Johns, MCC CAD Program | 3500 W. Balcones Center Dr., Austin, TX 78759
ARPA: johns@mcc.com | Phone: [512] 338-3714
UUCP: {uunet,harvard,gatech,pyramid}!cs.utexas.edu!milano!cadillac!johnslpringle@bbn.com (Lewis G. Pringle) (07/29/89)
In article <1974@cadillac.CAD.MCC.COM> rpj@redcloud.cad.mcc.com (Rich Johns) writes: > >I would like to be able to change the return type of the virtual >Copy(). G++ 1.35.1 rejects this, and I assume cfront would do the >same. I have only recently upgraded to g++ 1.35.1, and was previously >using 1.32 which allowed me to do this. Could someone explain why this >is not allowed. It seems like the compiler could easily determine >whether the change in return type were reasonable or dangerous. That >is to say, if the changed return type is derived from the return type >in the base class, let it be. I agree that this is a misfeature of the C++ language, and there are related problems which have hounded me. For much the same reason, I often want to tell the compiler that a data member (reference or pointer), is a (reference or pointer) to a subclass of the declared type. The problem, it that for data members it is ALMOST ALWAYS DANGEROUS, and that is why (assumtion) the compiler does not allow it. You see, a base class pointer could then be used to assign a bad value. For virtual functions arguments the same danger applies. However, the specific thing you suggested - return values - does appear to be safe, and so I too think it should be added to the language. Any base class calling your method cannot be bothered that the retuned type was really known to be a (pointer/reference) to a subtype of the type it expected. What the feature adds to the language is a type-safe way to help deal with a very common occurence: parallel subclassing in two hierarchies, one used by the other. All in all, I think its a good idea! BTW, I would be very interested in other peoples ideas about how to deal with this problem of having an owning class and an owned class, and subclassing both, and simply getting the right behavior. Example: ScrollBar has an data mamber Thumb. Now I want to make a ScrollBarWithSizeableThumb. I subclass both classes to the right thing, and add a SetSize() method to thunb (so the ScrollBarWithSIzeableThumb can reset it). Now the ScrollBar has an fThumb field of type (Thumb*). Do I use this and always cast? Do I use a new data member fThumb with the new type and shadow the parent class name? What is the best way to deal with this kind of problem???? "OS/2: half an operating system for half a computer." In Real Life: Lewis Gordon Pringle Jr. Electronic Mail: lpringle@labs-n.bbn.com Phone: (617) 873-4433
vaughan@mcc.com (Paul Vaughan) (08/01/89)
Lewis Pringle gives a good example of the issue at hand >Example: > ScrollBar has an data mamber Thumb. Now I want to make a >ScrollBarWithSizeableThumb. I subclass both classes to the right thing, >and add a SetSize() method to thunb (so the ScrollBarWithSIzeableThumb can >reset it). Now the ScrollBar has an fThumb field of type (Thumb*). Do >I use this and always cast? Do I use a new data member fThumb with the new >type and shadow the parent class name? What is the best way to deal with >this kind of problem???? >The problem, it that for data members it is ALMOST ALWAYS DANGEROUS, and that >is why (assumtion) the compiler does not allow it. You see, a base class >pointer could then be used to assign a bad value. >For virtual functions arguments the same danger applies. I've been considering ways that the compiler could verify correct handling of subclassed data members. I can't say that I've thought it out thoroughly, but here goes. The main idea is that the data member cannot be publicly accessable. This insures that only friends and member functions can deal directly with the data member. Now, it seems feasible to me that the compiler could simply verify that friends and members dealt with the data member in a type-safe fashion. This implies that any member function that assumes a type for the data member (that is, assigns a value, copies it via assignment, returns it as a value, passes it as a paramemter, etc.) must be virtual, and that the subclass must have it's own local definition for each such member function (and that the local definitions assign the right type). It also implies that a virtual function that assumes a type for the data member may not be called directly using a parent:: construct. I'm not sure how this would interact with constructors and the rules about how virtual functions act within them. It seems to me that these are all things that the compiler could verify without difficulty, thereby proving the program to be type-save without relying on programmer assertions (casts). The same argument applies for virtual function arguments, as long as the virtual function is protected. The function cannot be called, except from other member functions which are virtual and have a local definition. This might also work for subclassed return types, allowing virtual functions to return subclassed objects or pointers to subclassed objects. Andrew, you mention that there is a problem with type ambiguity if a member function could return a reference to a subclassed type. I don't think this is a problem given the rules outlined here. However, if you change your example to returning a pointer, rather than a reference, I'm not sure that there is a problem even with less restrictive rules. For instance, if you make that change, how does your example differ from this program: #include <stream.h> class A { public: int i; }; class B : public A { public: B() { j = 1;}; int j; }; main() { B* bp = new B; cout << "j = " << bp->j << "\n"; // prints j = 1 as expected A* ap = bp; // loses track of actual type here A a; a = *ap; // copies the A part into an A size space *ap = a; // copies the A part into a B size space cout << "j = " << bp->j << "\n"; // prints j = 1 (unchanged, as expected) delete(ap); // don't know what this does (memory leak?) cout << "works so far\n"; } Here the normal language rules specify that only the A part of *ap gets copied into a. In the reference case, where a reference to some potentially subclassed type is being assigned via a reference to another potentially subclassed type (as in your example), I would expect the target reference to be treated as the type visible at compilation time, (that is like the parent type). The value reference would naturally be treated like the parent type, just as the pointer is in the above program. Reclaiming the extra space and figuring out what to do with calling destructors is more difficult. But then, I'm not sure what happens in the above program when the delete(&a) is executed either. I don't understand the argument about overloaded functions either. Overloaded functions are already defined to only work on the type as visible at compile time. Of course, I see this as a weakness (but an efficient weakness) as compared to true runtime multiple dispatch (ala generic functions in CLOS), but that's a different issue. BTW, this issue is important to me. It comes up a lot when making libraries and toolkits of things for users to put together. Paul Vaughan, MCC CAD Program | ARPA: vaughan@mcc.com | Phone: [512] 338-3639 Box 200195, Austin, TX 78720 | UUCP: ...!cs.utexas.edu!milano!cadillac!vaughan