cok@islsun.Kodak.COM (David Cok) (02/13/91)
I and others recently completed a package of C++ classes in which we tried to provide both strong typing and inheritance and we ran into the following situation, in which the two requirements conflicted. We had a base class Image from which some derived classes DoubleImage, FloatImage, IntImage etc. were derived. We defined lots of relevant functions including (for example) operator- . Now intuitively operator- applied to a DoubleImage produces a new DoubleImage value and similarly for the other derived classes. With this and strong typing in mind, we defined member functions of the form const DoubleImage& DoubleImage::operator-() const; const FloatImage& FloatImage::operator-() const; But we also want some "virtualness" here. If we apply operator- to an Image we should get a new Image, having applied the correct - operator depending on the runtime derived type. That is we want: const Image& Image::operator-() const; The resulting partial program is Example A: class Image { public: virtual const Image& operator-() const = 0; }; class DoubleImage: public Image { public: const DoubleImage& operator-() const; }; BUT THIS IS ILLEGAL because the two operator- declarations do not return the same type. The best way we could find to give our desired interface was a kludge like this: Example B: class Image { public: virtual const Image& operator_minus() const = 0; inline const Image& operator-() const { return this->operator_minus(); } }; class DoubleImage { public: const Image& operator_minus() const; // the real work is here inline const DoubleImage& operator-() const { return (DoubleImage&)(((Image*)this)->operator_minus());} }; which has type-unsafe casts and twice the number of functions. Question 1: Does anyone have a better suggestion? Question 2: What is the rationale for requiring that the implementations of virtual functions in derived classes return exactly the same type as is declared in the base class? It would seem sufficient to require that the declaration in a derived class return a type which can be converted (using at least some set of implicit conversions) to that used in the base class. For example, if the base class returns Image& the derived class may return DoubleImage& ; if the base class returns Image*, the derived class may return DoubleImage* . This simple relaxation of the rule would not break any existing C++ code. The compiler can type-check everything just as well, and it would help avoid the problem of needing casts ("casts considered harmful...") to avoid types drifting to the top of the inheritance tree. It would have simplified our code above greatly -- almost every function needed a virtual internal function so that we could get the correct interface and virtual behavior as well. It does require the compiler to do a little work. Consider the rule relaxed to allow at least the pointer and reference conversions indicated above, and add to example A above the corresponding declaration of FloatImage::operator-(). Applying operator- to a FloatImage& would simply return a FloatImage&. Applying operator- to an Image& needs to return an Image&. The compiler needs to create a version of FloatImage::operator- which converts its FloatImage& result to an Image& and a version of DoubleImage::operator- which converts its DoubleImage& result to an Image& (change all references to pointers if you like). For these conversions that is trivial; more complicated conversions (which may not be allowed) or the presence of MI would mean some real work had to be done. In effect, the compiler converts example A into example B, but does so in a type-safe manner. When the programmer does it with casts, type-checking is lost. David R. Cok Eastman Kodak Company cok@Kodak.COM 716-477-7086