[comp.lang.c++] For Philosophers and Lawyers

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