Xref: utzoo comp.lang.c++:11675 comp.std.c++:600 Newsgroups: comp.lang.c++,comp.std.c++ Path: utzoo!utgpu!craig From: craig@gpu.utcs.utoronto.ca (Craig Hubley) Subject: Re: Co-ordinating the polymorphism in C++ Message-ID: <1991Feb16.114422.14266@gpu.utcs.utoronto.ca> Followup-To: comp.std.c++ Organization: Craig Hubley & Associates References: <1991Feb11.003849.27340@gpu.utcs.utoronto.ca> <600@taumet.com> Distribution: na Date: Sat, 16 Feb 1991 11:44:22 GMT Since there was a recent post about this very issue in comp.std.c++, since it's a question about the desirability of changing something in the language, and a stream was already going on this subject here, I have cross-posted this and directed followups to comp.std.c++, which I believe is the appropriate forum for such discussions. In article dsouza@optima.cad.mcc.com (Desmond Dsouza) writes: > >In article <600@taumet.com> steve@taumet.com (Stephen Clamage) writes: > > craig@gpu.utcs.utoronto.ca (Craig Hubley) writes: > > |Now that C++ has three polymorphism mechanisms > | - overloading (compile-time) > | - templates (link-time) > | - virtual functions (run-time) > > |Is anyone thinking about how to reconcile them ? As it stands now, I can > |use overloading to add extra arguments to functions, etc., but I can't do > |it in my derived classes because virtuals can't be overloaded, etc. I stated this badly. They can't be overloaded in the way I want, that is, to allow pointers to derived classes as return values in place of pointers to bases, and pointers to base classes as arguments in place of pointers to derived, and have such an overload override the original function. That is, I want the compiler to override the base class's function wherever a derived function is defined that - accepts a parameter list convertible to one the base would accept - returns a value of a type convertible to one returned by base my definition of "convertible" is quite restrictive here: pointers to derived classes, which are legal substitutes for pointers to bases. I am interested in discussing expanding this definition to include other builtin conversions, but I do not think it should extend to user-defined ones. For one thing, the overriding rules should NOT depend on user-defined code, which can change and cause unexpected chaos. But the builtin conversions will not change in this fashion, and are largely there to ease the pains of dealing with the C data types, and to manage inheritance. Other OO languages permit such type-compatible overloading, and in some languages (e.g. Trellis) it is enforced. That is, you can only define changes that will leave a derived type a legal substitute for a base type in all circumstances (i.e. type-safe). > Overloading need have no relation to polymorphism. > Templates and polymorphism are orthogonal concepts. > Virtual function may be overloaded. > >The rules for overloading resolution with class derivation is >remarkably similar to the virtual function mechanism (in fact, it >resembles 'multi-methods' with dispatch based on the derivation >closeness of *all* arguments, instead of just the 'this' pointer), >except the work is done at compile-time. I suppose I am suggesting they ought to be one mechanism, not two. Overloaded functions should also be permitted to return "more specialized" types and accept "more general" arguments. >Templates are the C++ implementation of what Cardelli called >'parametric polymorphism'. You could say templates and class >derivation/virtuals are orthogonal, except that template-instantiated >classes could have meaningful class-derivation relationships between >them. e.g. read-only-list Vs read-only-list Thanks for the reference. To invoke Trellis again, its type-generators (equivalent to templates) can be declared "not_flat" which simply means that the generated types (e.g. collections) have the same inheritance relationship as the type used to generate them. >Virtual functions may NOT be overloaded in the manner Craig seems to want. Not at present, although I think a strong case can be made for the change. >A derived class,D, with a virtual functions D::f whose arguments >differ from those of its base class,B, HIDES the base class function, >and does not override B::f. Doing otherwise breaks type-safety. See This is a rather narrow notion of type-safety. I would argue that the general theory does not apply here. C++ recognizes distinct types but also blurs the distinction between some types (e.g. pointers to base vs. derived, numerical types) with its unique builtin type conversion. This causes some things of "different" type to be guaranteed to be treated as if they were the same, so long as not more than one user-defined conversion is invoked, and I am suggesting we leave them out of this completely. In this case, the argument list may well be convertible to one that B::f will accept, and with only builtin conversions being invoked. There is no change to any user program that will cause this conversion to be invalid, thus this argument list is always a valid substitute for that of B::f. > craig@gpu.utcs.utoronto.ca (Craig Hubley) writes: > > >To propose one small change, if virtuals could be overloaded in type- > >compatible ways (i.e. redefining acceptable rguments as pointers to base > >classes in place of pointers to base classes, returning pointers to derived > >classes in place of pointers to base classes, and permitting extended > >argument lists where defaults have been provided) this would add no more > >work to the compiler than a simple decision to exhaust matching on virtual > >functions before trying for an exact match at a different level, and no > >overhead at all to user programs that did not use these features. > >In effect it is no more than removing some arbitrary constraints on typing > ^^^^^^^^^ > >that are inconsistent with the rules in the rest of C++, which is exactly > >the kind of change we have been seeing between versions of C++. > >'Arbitrary' ? Thats kinda strong! Sometimes inconvenient, yes. I will stick to my gadfly term. Every other language that pretends to be OO at least permits, usually encourages and sometimes enforces what I propose. It arises from sound principles, and C++ is arbitrary in flouting them without a clear reason that is stated in practical terms, not with abstract terms like "type safety" which I have demonstrated mean something different in C++ than in other languages. Second, C++ itself permits data values of these types to be freely substituted but somehow flinches at permitting functions that return these types from being similarly substituted (i.e. overloaded functions can't return the more specialized type). Thus it is inconsistent with itself. >1 Redefining acceptable arguments to be pointers to derived classes > (I'm assuming thats what you meant) instead of to base classes will > break strong typing. No, the opposite. You can *generalize* arguments but not *specialize* them without risking failures. The derived virtual may try to use aspects of the derived type it expects, when only the base type is there, since that was what the original base virtual expected, and that was what programmers provided. That would be equivalent to automatically casting B* to D*... which C++ doesn't do. If the base class has a virtual thus: class B { virtual B* foo(D*); } Then this is what we are talking about (comment from a previous post) class D : public B { // D* is always demotable to B*, so the function call B* foo(B*) {}; // D::foo(D*) should be equivalent to D::foo((B*)D*) // It could also be interpreted as B::foo(D*), which // might be why it's illegal, but a simple rule to // exhaust argument matching on virtuals before trying // the base functions (if they are tried at all) would // resolve this ambiguity. } And this is what we were talking about above: class E : public B { D* foo(B*) {}; // Similarly, any context that ends up calling // D::foo() where B::foo() is expected will // receive a D* value that can be used as a B*. // But C++ says that overloads and virtuals // can't change the return type at all. Why not // allow it where the objects are the same size // and C++ already has a built-in promotion ? } You are "doing the same" (D) or "doing more" (E) with "less information". And here are the consequences: // if all of the above were allowed, extending C++ consistently with the // rules mentioned above yields the following results: // main { B* b; D* d; E* e; b->foo(b); // illegal, fine, B* shouldn't automatically promote b->foo(d); // legal, exact match, return B* d->foo(b); // legal, exact match, return B* d->foo(d); // legal, D::foo((B*)d), return B*, ignore B::foo(D*) e->foo(b); // legal, exact match, return D* e->foo(d); // legal, E::foo((B*)d), return D*, ignore B:foo(D*) } Whether there are any other consequences, is up to us to determine. :) >2 Returning pointers to derived classes in place of pointers to base > classes requires a change to function call/return sequences, > particularly if the implementation follows the cfront model of > laying out multiply-inherited classes and virtual tables. It can, > however, be done without compromising type safety. Agreed. I didn't expect it would be a picnic for compiler writers. It is people using and writing code I am thinking about. There was an excellent post in comp.std.c++ showing the kind of problems that are created by NOT having this mechanism. >3 Extended argument lists in the derived class would definitely change > the function call sequence, since optional arguments are currently > completely resolved at compile time. We haven't really talked about the implications of allowing extended lists. One issue with such a change would be that calls to virtual functions using the extended arguments are incompatible with the original base function. Perhaps the compiler should reject any such call where the base (with the shorter argument list) is one of the object's potential types. In other words, it's only legal where I declare, say, a D* or pointer to a type derived from D, but not where I declare a B*. It would be nice to have a way to test the actual type, and do type-specific things in the case arm where the type is guaranteed to match, but C++ doesn't allow programmers to see the darn type tag at all. >Note that you *can* have these three together: >a. B::f(B*) >b. D::f(B*) >c. D::f(D*) > >But b) overrides a), while c) does not override a). Yes. This is fine. But my point is given: d. B* B::f(D*) e. B* D::f(D*) f. D* D::f(D*) g. D* D::f(B*) h. B* D::f(B*) e) overrides d) f) should be allowed and overrides d) (compile error if e) already exists) g) should be allowed and overrides d) (compile error if e) or f) exists) h) for completeness, should be allowed, overrides d), unless e)-g) exist) Calls to d) plus the builtin conversion would invoke f)-h) correctly. Values returned by f) and g) are converted by builtins to those acceptable by any context originally calling d). >1 --> really boils down to the > "contravariant Vs covariant" >controversy. The contravariant rule, which guarantees strong typing, >does NOT allow D::f(D*) to override B::f(B*). The covariant rule >allows it, but can fail at run-time in some circumstances. This isn't what I'm proposing, as I make clear above. I'm against runtime failure in C++, and as stated above I don't think the general principles of strong typing can be applied to C++ unless one deals explicitly with the builtin conversions. >I think some of these limitations can be avoided if the compiler is >allowed to generate 'customized' code, like the Self compiler does. >e.g. 2 versions of D::f() -- one to be called from a B* object, the >other from a D* object (or multiple entry points) and emit some >run-time type checks and type conversions in the B* version. This is a possibility, I see no reason for C++'s compiler to always default towards braindeath, and it satisifies the rule that it costs nothing if you don't ever try to use this feature. But as Bjarne said, this would "bless contravariance". >but thats a pretty major change :-) I agree. I don't see much reason for it. C++ is designed to let you do this sort of thing for yourself, to some degree, although testable type tags are certainly required to do it right. You just gotta SEE the type of the object sometimes. >Desmond D'Souza. > > Desmond D'Souza, MCC CAD Program | ARPA: dsouza@mcc.com | Phone: [512] 338-3324 > Box 200195, Austin, TX 78720 | UUCP: {uunet,harvard,gatech,pyramid}!cs.utexas.edu!milano!cadillac!dsouza -- Craig Hubley "...get rid of a man as soon as he thinks himself an expert." Craig Hubley & Associates------------------------------------Henry Ford Sr. craig@gpu.utcs.Utoronto.CA UUNET!utai!utgpu!craig craig@utorgpu.BITNET craig@gpu.utcs.toronto.EDU {allegra,bnr-vpa,decvax}!utcsri!utgpu!craig