williams_j@apollo.HP.COM (Jim Williams) (05/18/91)
I would like to throw out some ideas for discussion. Hopefully these concepts have not already been discussed to death. --------------------------- Example 1 ------------------------------------------ class A; class B; B* f(A* ap) {return (B*)ap;} -------------------------------------------------------------------------------- The compiler accepts the above code with no problems. But how can the complier cast an A pointer to a B pointer? The compiler doesn't know anything (yet) about A or B. Is A derived from B? Is B derived from A? If B is virtually derived from A, the cast in f() is illegal, but the compiler, not knowing if B is virtually derived from A, signals no error. Does this make sense? Actually the problem derives from the fact that type casting of pointers to classes can have two VERY different meanings. The first is simply change the type without changing the binary representation. The second is as the primary means for expliting the polymorphic nature of class objects. It is an anachronism from single inheritance C++ that these two meanings had identical implementation. Unfortunately they now have different implementations but still have the same syntax. The following example exibits this ambiguity in a much more subtle way. --------------------------- Example 2 ------------------------------------------ class A { int take_up_space; }; class C; // this declaration is required so B::next() can be defined class B { B* next_b; // this is a pointer to another B; public: C* next() {return ( (C*)next_b ); } // this function accesses next_b, }; // and casts it. class C: public A, public B {}; main() { C* cp; // .... cp=cp->next(); // This statement doesn't work right! //... } -------------------------------------------------------------------------------- The cast from a B* to a C* requires subtracting an offset since B is offset by the size of A within C. In the above example, the compiler doesn't subtract the offset because it doesn't know that C is derived from B at the time that the inline function next() is defined. This can be easily fixed by changing the order of declarations. --------------------------- Example 2a (fixed) --------------------------------- class A { int take_up_space; }; class C; // this declaration is required so B::next() can be defined class B { B* next_b; // this is a pointer to another B; public: C* next(); // this function accesses next_b, }; // and casts it. class C: public A, public B {}; inline C* B::next() {return ( (C*)next_b ); } // move this declaration main() { C* cp; // .... cp=cp->next(); // Now it works! //... } -------------------------------------------------------------------------------- This whole scenario raises the question in my mind as to wheather enough attention was paid in the design of the language to casting pointers to base types to pointers to derived types. This seems like something that many language users would want to do. There would be a simple way to implement type safe casting of pointers to classes. The compiler could assign a unique type identifier for each class type (perhaps the address of a static member). Then at the end of the virtual table, it would add a list of ordered pairs consisting of class type identifiers and the corresponding offset that needed to be added to the pointer to cast to that type. This scheme may not be the best way, but it is one way to accomplish this. When a cast needed to be done at run time, the list would be searched to find the needed offset. If the type identifier is not found in the list, then the cast is illegal and a run time error or exception would be signaled. This would allow casting from pointers to base to pointers to either non-virtual or virtual derived types. It could also allow for casting between unrelated types provided that the object pointed to was derived from both types. There are a number of problems with this. Because of inefficiencies, it could make sense to have a compiler switch to turn off checking. Often an unchecked cast can be done far faster than a checked cast. Also this scheme assumes the existance of a virtual table when in fact there may not be one. The compiler has no way of knowing if a given class will require a virtual table for casting. The cost of adding a virtual table and virtual table pointer to every class would make that an impractical solution. Possibly the best compromise would be to use the virtual table if it is there. If there is no virtual table, do the cast without checking if possible, and issue a compile time warning. If the cast can't be done without a virtual table and there isn't one, issue a compiler error. The programmer can always add a member function "virtual void do_nothing() {}" to force the creation of a virtual table if it is necessary. If it is desired to simply change the type without changing the binary representation, the following syntax could be used "bp = (B*)(void*)ap;". If type checking could be done, it seems within the spirit of the language to allow implicit as well as explicit casting from pointers to base to pointers to derived types. I'm sure there are other problems that I havn't thought of, but this area seems worthy of thought and discussion.
purtill@morley.rutgers.edu (Mark Purtill) (05/18/91)
williams_j@apollo.HP.COM (Jim Williams) writes: >I would like to throw out some ideas for discussion. Hopefully these >concepts have not already been discussed to death. >--------------------------- Example 1 ------------------------------------------ >class A; >class B; >B* f(A* ap) {return (B*)ap;} >-------------------------------------------------------------------------------- >The compiler accepts the above code with no problems. But how can the complier >cast an A pointer to a B pointer? The compiler doesn't know anything (yet) >about A or B. Is A derived from B? Is B derived from A? If B is virtually >derived from A, the cast in f() is illegal, but the compiler, not knowing if >B is virtually derived from A, signals no error. Does this make sense? Let's call things like "class A;" *class declarations* and the actual "class A {...};" the *class definition*. An easy way to solve this problem would be to require a class declaration to list the bases classes of the class defined. So your example might turn into: class Q ; class X ; class Y ; class Z ; class M : public virtual Q, public X, protected Y, Z ; class N : public virtual Q, X ; class A : public N, M ; class B : virtual A ; ... which is long, but allows the compiler to figure out how to generate the cast correctly. >--------------------------- Example 2 ------------------------------------------ >class A { > int take_up_space; > }; >class C; // this declaration is required so B::next() can be defined >class B { > B* next_b; // this is a pointer to another B; > public: > C* next() {return ( (C*)next_b ); } // this function accesses next_b, > }; // and casts it. >class C: public A, public B {}; So, in my scheme, an error would be signaled here telling you that you declared C to have no base classes and now are defining it with some base classes. The advantage of this idea is that no run-time errors need ever be generated since the compiler will always have enough information to do it's job correctly. The main disadvantages I can see are (1) you could get long strings of class declarations (as in my rewritten example one); and (2) some existing code will generate errors. To help avoid code breaking, we could say that the error would only be issued in case of multiple inheritence or virtual base classes, so that most current code will be okay (and a good chunk of that that will break won't work correctly now anyway). The error message could be phased in: for now, a warning would be generated, and only in later versions would the user get the actual error. So the proposal would be (with the terminology above): Declarations of classes are allowed to list the base classes of the declared class. (I'm not sure that this is allowed now, although g++ accepted an example). A declaration of a class must match the definition of that class (that is, it must list the same base classes with the same specifiers for each base class) except that a declaration without any base classes may have one non-virtual base class (although a warning may be issued and this feature may be deleted in the future). If the declaration does not match the definition, the behaviour of casts involving the class in question (and pointers to that class, and so on) is undefined for all casts that are in the scope of the incorrect declaration but not the definition, and warning(s) may be issued. (In future, an error may be generated). Note that if the definition and the incorrect declaration are in different files, it may not be possible for the compiler to detect the problem at all. Any other problems? Should it require that the list of base classes be in the same order as well? Comments welcome! ^.-.^ Mark Purtill purtill@dimacs.rutgers.edu (908)932-4580 (O) ((")) DIMACS, P.O. Box 1179, Rutgers U., Piscataway, NJ 08855 (908)220-6905 (H) ********** Note new area code! We are now (908) rather than (201)! **********
jimad@microsoft.UUCP (Jim ADCOCK) (05/24/91)
In article <May.17.17.47.20.1991.1598@morley.rutgers.edu> purtill@morley.rutgers.edu (Mark Purtill) writes: |williams_j@apollo.HP.COM (Jim Williams) writes: |>class A; |>class B; |>B* f(A* ap) {return (B*)ap;} | |>The compiler accepts the above code with no problems. But how can the complier |>cast an A pointer to a B pointer? The compiler doesn't know anything (yet) |>about A or B. Is A derived from B? Is B derived from A? If B is virtually |>derived from A, the cast in f() is illegal, but the compiler, not knowing if |>B is virtually derived from A, signals no error. Does this make sense? .... C++ allows these unsafe casts so that programmers can use pointers to objects without always having to include class declarations. As long as a class doesn't actually do anything with the object pointed-at, the class declaration may not be needed. Virtual bases and MI break these bitwise-equivalent casts, but on the other hand, I don't think you'd want to make C programs or C++ programs only using SI pay the penalty for supporting MI and virtual bases. Mark's suggestions are similar to discussions on comp.std.c++ about support for dynamic type information. Interested readers may wish to review the discussions there.
jbuck@forney.berkeley.edu (Joe Buck) (05/24/91)
This thread addresses problems with casts between pointer types that are declared (i.e. "class A;") but not defined, and the problems that arise with multiple inheritance. Jim Adcock points out that >C++ allows these unsafe casts so that programmers can use pointers to >objects without always having to include class declarations. This is a very strong argument; those of us that want our large C++ projects to compile in reasonable time rely on incomplete class declarations. Also, E&S discusses this very issue on pp. 68-69. They give the example class S; class T; T* f(S* p) { return (T*)p;} class A { int a;}; class S { int s;}; class T : public A, public S { int t; }; This code will do The Wrong Thing. Their conclusion? "It follows that where multiple inheritance is involved casting to and from undefined types is best avoided." -- Joe Buck jbuck@galileo.berkeley.edu {uunet,ucbvax}!galileo.berkeley.edu!jbuck