Topic: Proposed implementation of contravariance on function return type


Author: cok@islsun.Kodak.COM (David Cok)
Date: Mon, 1 Apr 91 13:21:05 GMT
Raw View
In a recent post (<1991Mar25.220712.71518@microsoft.UUCP>) Jim Adcock claimed
that contravariance on function return type causes costs for all functions,
not just contravariant ones.  In this article I dispute that by providing an
implementation which
 -- adds cost only to functions which use contravariance
 -- the additional cost is very minimal (an add or two per call of
  a contravariant function)
 -- fits in directly with current typical implementations of virtual
  functions
 -- works in the environment of separately compilable source files

If implementation problems are what is preventing this concept from being
included at the ANSI level, someone please tell me whom to contact.  I firmly
believe that adding contravariance on return type adds simplicity and elegance
to programs which need it, and that the need is not uncommon: I can't see how
to write a sensible clone function in C++ without it (by sensible I mean
without using casts or an additional hidden helper function and even those do
not work for clone in MI where more than one base class defines the same
function).

BACKGROUND

Consider a chain of classes D0, D1, ... Dn ... where Di is derived from D(i-1)
and perhaps from other classes (i.e. MI is allowed here), and that each class
in the chain defines a virtual function f with the same argument types.  Then
function Di::f overrides D(i-1)::f, and Di::f is executed on an object of
class Di even if it is accessed by means of a pointer or reference to Dj  (j<i).
This is conventional inheritance.

The current rules (ARM 10.2 p.208 "It is an error for a derived class function
to differ from a base class' virtual function in return type only.") require
that the return types of the various functions Di::f must all be the same.  The
proposal of "contravariance on function return type" means (to me) to relax
this rule to read something like

"The return type of the derived class function must be either
 a) the same as the return type of the base class function, or
 b) be a pointer to a class X, where the base class function returns a
    pointer to a class Y and X is derived (perhaps indirectly) from Y, or
 c) be a reference to a class X, where the base class function returns a
    reference to a class Y and X is derived (perhaps indirectly) from Y."

Note that the argument types of all the functions must still be the same.  There
have been proposals to relax that rule, but I am not proposing that.

Case a above is the current rule.  No changes are necessary to implement those
virtual functions.  Case b is discussed below and case c is completely
analogous to case b.

In general we do not have a chain of classes but a DAG of classes which
inherit the function f from each other.  Two complicating situations are
(1) a class may inherit the function f from more than one of its base classes
and (2) there may be virtual base classes involved among the function return
types.


SEPARATE COMPILATION

Separate compilation requires that some parts of code be able to be compiled
without knowledge of other parts.  In particular, the entire base class D0 and
the definitions of its functions, including f, must be able to be compiled
without knowledge of any of the classes derived from it.

However, a derived class Di inherits from D(i-1) and indirectly from all the
Dj with j<i.  The declaration of class Di is not legal unless there have been
prior declarations of the Dj (j<i) within the same source file (commonly
by means of include files).  Thus when the compiler is compiling the declaration
of Di, it can know whether there is any case of contravariance in Di or the
Dj from which Di inherits.

Similarly, any use or definition of Di::f must be preceded by the declarations
of Di and Dj (j<i), so it can be known when Di::f is used or defined that there
is contravariance to be dealt with.  However, when f is invoked on a D0*, it is
not known at compile time which Di D0* points to.

The corollary to this is that there can be  no change to the implementation of
the class Di and the syntactic use or definition of the virtual function Di::f
so long as the return type of Di::f is the same as the return type of D0::f
for j<i.  Any changes may only be in the chain at and after the point of the
first contravariant function.

IMPLEMENTATION

The implementation will be presented in full generality first.  Rest assured
that, as the examples which follow demonstrate, simple cases actually turn out
to have simple modifications.  My first attempt at this fell into the trap of
a case analysis where the cases were situations of increasing complexity.  I
realized that the solutions for all situations were special cases of one
general set of rules, which made for one elegant prescription for implementing
contravariance.  Unfortunately, the rationale for the general case is not so
obvious until one works through some examples.

We are concerned with the DAG of classes which inherit the virtual function
f from each other.  For a class c, the return type of c::f is denoted by Rc*.
I will say that c multiply-inherits f if c inherits f from more than one of
its base classes.  I will say that class a is an ancestor of class c if c
is derived from a, possibly through other intermediate classes.  Class c is
virtually-inherited from a if at least one of the classes in the inheritance
chain from a to c is a virtual base class of the next class on the chain.

For classes b and c within the DAG for function f,
if b is a base class of c, then Rb is equal to or an ancestor of Rc (by the
rules for contravariance).  Then if b is an ancestor of c, Rb is equal to
or an ancestor of Rc.

Recall that each object of class c contains within it all its ancestors,
and that for each ancestor a there is a (in typical implementations) virtual
function table which indicates, for all the functions inherited from a, which
actual function to call on the object of class c.  In practice, many of these
virtual function tables are shared and even the pointers to the virtual
function tables are shared.  The problem in contravariance begins with the
fact that the functions d::f for the descendants d of a class a return Rd* so
the function dynamically called via an_a->f() on a pointer to an object of
class a returns a variable type.  The implementation described below corrects
this problem by changing the return types to something fixed, namely Ra'*, and
then using (automatically introduced) casting in the program text to bring
the return type back to what the programmer originally specified.

The implementation of contravariance consists in making the following
modifications to the programs and the virtual function tables.

For each class c in the DAG for f, choose a class Rc' which satisfies
 a) Rc' is equal to or an ancestor of Rc
 b) Rc is not virtually-inherited from Rc'
 [ I give below a prescription for the Rc' which optimizes the
   implementation]

Then make the following modifications:

1) Change the return value of c::f from Rc* to Rc'*.  This is a legal cast
since Rc' is equal to or an ancestor of Rc.  In implementation it is a simple
add of a compile-time constant to a pointer.  The cast is unnecessary if Rc
is the same as Rc'.

2) Change the virtual function table entry for function f of class a in an
object of class c: ordinarily it would be &c::f (which has been modified in
rule 1 to return a  pointer to the Rc' part of an Rc object; instead make the
vtable entry point to the thunk   (Ra'*)(Rc*)c::f
[ Before you get worried, I'll promise that most of the time this will be
equal to &c::f and the vtable will not have to be changed.]
Since c::f, as modified by rule 1, returns a pointer to the Rc' part of an
Rc object and Rc is not virtually-inherited from Rc', the (Rc*) cast is ok;
since Ra' is equal to or an ancestor of Ra, which is equal to or an ancestor of
Rc, the (Ra'*) cast is legal.  If Ra' is equal to Rc' the casts do nothing
and &c::f can be left in the vtable.

3) Change every use of f in an expression as follows:  Let c be the STATIC
class of the object to which f is being applied (after applying this rule to
the expression to which f is being applied, if appropriate); then cast the
result of applying f with the cast (Rc*).  This makes sure that the type of
the final result of applying f to an object of class c is Rc*, as the original
declaration of the function called for.  For example if c_ptr is statically
declared to be a variable of class c*, then (c_ptr->f()) becomes
((Rc*)(c_ptr->f())); also c_ptr->f()->f() becomes
  ((Rd*) ( ((Rc*)(c_ptr->f())) -> f() ) )
where d == Rc.  This cast is again a simple add of a compile time constant
to the pointer which is returned by the function f.  If the function f is
applied to an object of class c, the thunk (Rc'*)(Rc*)c::f is invoked which
returns a pointer to the Rc' part of an Rc object.  If the function f is
applied to a pointer to the c part of a d object, the thunk (Rc'*)(Rd*)d::f is
invoked, which returns a pointer to the Rc' part of a Rd object (c being an
ancestor of d).  In either case the cast from Rc'* to Rc* is ok and
accomplishes what the user intended, namely that c_ptr->f() should return an
Rc*.  This cast is unnecessary if Rc is the same as Rc'.
NOTE: If f is used by having its address taken, a new function will have to
be created which implements (Rc*)f, and its address taken instead.

How should the Rc' be picked?  The criteria are (1) to require no changes
to functions or vtables in cases (given separate compilation) where it is not
known that contravariance exists, (2) to avoid the necessity for thunks when
possible, and (3) to retain sharing of virtual function tables and pointers.
Criterion 1 is required.  Criteria 2 and 3 can be accomplished in all cases in
which f is not multiply inherited and there are no virtual base classes
involved; in particular, if there is no MI (and thus no need for virtual base
classes) no thunks are necessary and this scheme could be implemented by a
front-end to CC (that is by a CCfront!).  The key is to choose Rc' to be equal
to Ra' in rule 2 above.  The following procedure accomplishes that.


 Beginning at the top (most Base-ic classes) of the DAG for the function
 f, choose Rc' as follows:

 Case 1: c does not inherit f from any of its base classes (i.e. is a
    leaf of the DAG for f): then choose Rc' = Rc.  [This rule is
    necessary since when the compiler has seen only this class, it does
    not know that there is any contravariance in f, so it will know
    neither to apply rule 1 to the definition of f nor to apply rule 3
    to the uses of f.]

 Case 2: c inherits f from some number of base classes b1, b2 ,...:
 a) If Rc is virtually-inherited from all of the Rbi', then Rc' = Rc.
 b) If Rc is non-virtually inherited from at least one Rbi', then
  Rc' = Rb' where b is the first class in the list of base classes
  for c for which Rc is not virtually-inherited from Rb'.
  [ The reason for picking the class nearest the beginning of the
  list of base classes for c is that if b is actually the first
  class in the list, the virtual function table for a b object
  within a c object can still be shared with the vtable for the
  c object.]
 NOTE: It does not matter whether there are virtual base classes among
 the b1..bn, just whether there are virtual base classes involved in the
 function return types.

EXAMPLE 1: No multiple-inheritance of f and no virtual base classes.

Consider this example of a clone function in a simple hierarchy.
(Assume that neither X nor Y declare or inherit a virtual member function f.)

class D0 {
 D0();
 D0(const D0&);
 virtual D0* clone() { return new D0(*this); }
};

class D1: public X, public D0 {
 D1();
 D1(const D1&);
 virtual D1* clone() { return new D1(*this); }
 virtual D0* up() { return (D0*)this;}
};

class D2: public Y, public D1 {
 D2();
 D2(const D2&);
 virtual D2* clone() { return new D2(*this); }
 virtual D1* up() { return (D1*)this;}
};

Then a program could include these statements and expressions:
 D0 d0; D1 d1; D2 d2;
 D0* d0p; D1* d1p; D2* d2p;

 d0.clone(); // returns a pointer to a cloned D0
 d1.clone(); // returns a pointer to a cloned D1
 d0p = &d1;   // a pointer to the D0 part of a D1
 d1p = &d1;   // a pointer to a D1
 d0p->clone(); // returns a pointer to the D0 part of a cloned D1
   //      (copy of d1)
 d1p->clone(); // returns a pointer to a cloned D1 (copy of d1)
 d0p = &d2;   // a pointer to the D0 part of a D2
 d1p = &d2;   // a pointer to the D1 part of a D2
 d2p = &d2;   // a pointer to a D2
 d0p->clone(); // returns a pointer to the D0 part of a cloned D2
   //      (copy of d2)
 d1p->clone(); // returns a pointer to the D1 part of a cloned D2
   //      (copy of d2)
 d2p->clone(); // returns a pointer to a cloned D2 (copy of d2)
 d2p->up(); // returns a pointer to the D1 part of d2
 d2p->up()->up(); // returns a pointer to the D0 part of d2
 d1p->up(); // returns a pointer to the D0 part of d2

For clone():
We have R0 = D0, R1 = D1, and R2 = D2.  The rules for choosing the R' dictate
that R0' = R0 = D0, R1' = R0' = D0, and R2' = R1' = D0.
For up():
R1 = D0, R2 = D1.  R1' = R1 = D0, R2' = R1' = D0.
Indeed, for any hierarchy for a contravariant function without virtual base
classes and without the function being multiply-inherited, all the R' will be
equal to the return type of the most basic class.

The application of the implementation does this:
rule 1) in the definition of each clone function, change the result type to D0*.
 in the definition of each up function, change the result type to D0*.
rule 2) since Ra' = Rc' for any combination of classes, no changes to the
 virtual function tables are needed and no thunks are introduced,
rule 3) in each use of the clone function, cast the result returned by the
 clone function to the static type of the object to which it is applied.
 in each use of the up function, cast the result returned by the up
 function to the base class of the static type of the object to which it
 is applied.

For the example given above, each clone and up function would be modified to
return a (D0*), and the subsequent code would be modified to be this:

 d0.clone(); // returns a pointer to a cloned D0
 ((D1*)(d1.clone())); // returns a pointer to a cloned D1
   // d1.clone() returns a pointer to the D0 part of a D1
   // which is then cast back to a pointer to the D1
 d0p = &d1;   // a pointer to the D0 part of a D1
 d1p = &d1;   // a pointer to a D1
 d0p->clone(); // returns a pointer to the D0 part of a cloned D1
   //      (copy of d1)
 ((D1*)(d1p->clone())); // returns a pointer to a cloned D1 (copy of d1)
   // d1p->clone() returns a pointer to the D0 part of a D1
   // which is then cast back to a pointer to the D1
 d0p = &d2;   // a pointer to the D0 part of a D2
 d1p = &d2;   // a pointer to the D1 part of a D2
 d2p = &d2;   // a pointer to a D2
 d0p->clone(); // returns a pointer to the D0 part of a cloned D2
   //      (copy of d2)
 ((D1*)(d1p->clone()); // returns a pointer to the D1 part of a cloned D2
   //      (copy of d2)
   // d1p->clone() returns a pointer to the D0 part of a D2
   // which is then cast back to a pointer to the D1 part
 ((D2*)(d2p->clone()); // returns a pointer to a cloned D2 (copy of d2)
   // d2p->clone() returns a pointer to the D0 part of a D2
   // which is then cast back to a pointer to the D2
 (D1*)(d2p->up()); // returns a pointer to the D1 part of d2
   // d2p->up now returns a D0* which is cast back to D1*
 ((D0*)( ((D1*)(d2p->up())) ->up());
   // returns a pointer to the D0 part of d2
   // The static type of ((D1*)(d2p->up())) is D1* though
   // the D1 pointed to is part of a D2 object; so the
   // result of applying up() to this D1* is cast to R1*
   // which is D0*
 d1p->up(); // returns a pointer to the D0 part of d2; contravariance is
   // not necessarily known when this function is seen

In Jim Adcock's example:

> Base
> {
> ....
> public:
>  virtual  Base* doSomething();
> ....
> };
>
> void usesDoSomething(Base* bp)
> {
>  bp = bp->doSomething();
>  bp = bp->doSomething();
> }
>
> class Derived: public Foo, public Base
> {
>  Derived* doSomething();
> }

the function usesDoSomething is not modified at all, which is great because, as
he pointed out, the compiler  wouldn't know that it needed to.  Instead
Derived::doSomething() is modified to return a Base* and the user's code

void usesDoSomething(Derived *dp) {
 dp = dp->doSomething();
 dp = dp->doSomething();
}

is modified automatically by the compiler to

void usesDoSomething(Derived *dp) {
 dp = ((Derived*)(dp->doSomething());  // doSomething now returns a Base*
 dp = ((Derived*)(dp->doSomething());  // doSomething now returns a Base*
}

No runtime casts are needed because the virtual functions (doSomething) are
modified to return Base* (i.e. D0*).  The casting to the type the user wanted
can be done statically.  The user could do it him or herself, but only by
putting casts everywhere in the code which is unsafe and unsightly.


EXAMPLE 2. -- multiply inherited functions

Now suppose that a class D1 inherits f from both Da and Db.  If the
corresponding Ra and Rb are identical, we can proceed as in case 1.  The
additional difficulty comes when Ra and Rb are different.  R1 is required
to be a descendant of both of them.  The clone function is a good example:

class Da {
 Da* clone() { return new Da(*this); }
};

class Db {
 Db* clone() { return new Db(*this); }
};

class D1: public Da, public Db {
 D1* clone() { return new D1(*this); }
};

class D2: public D1 {
 D2* clone() { return new D2(*this); }
};
{
D1 d1;
Da* dap = &d1; Db* dbp = &d1; D1* d1p = &d1;

d1p->clone(); // a pointer to a D1 which is a copy of d1
dap->clone();// a pointer to the Da part of a D1 which is a copy of d1
dbp->clone();// a pointer to the Db part of a D1 which is a copy of d1
}{
D2 d2;
Da* dap = &d2; Db* dbp = &d2; D1* d1p = &d2; D2* d2p = &d2;

d2p->clone(); // a pointer to a D2 which is a copy of d2
d1p->clone(); // a pointer to the D1 part of a D2 which is a copy of d2
dap->clone();// a pointer to the Da part of a D2 which is a copy of d2
dbp->clone();// a pointer to the Db part of a D2 which is a copy of d2
}

According the the prescription above, Ra' = Ra = Da, Rb' = Rb = Db,
R1' = Ra' = Da (since Da is before Db in D1's list of base classes), and
R2' = R1' = Da.

The definition and use of Da::f and Db::f and the vtables in Da and Dob
are unchanged.  For a D1 object we (1) modify D1::clone to return a (Da*),
and cast the result of each application of clone to objects with static type
D1 to (D1*); the virtual function tables end up like this:

            __________
 D1 object:   Da part |        |
        |   -----|-- vtable for Da in a D1 object
        |        |      and D1 in D1:
        |        |  clone: &D1:clone
            |________|
              Db part |        |
        |   -----|-- vtable for Db in a D1 object:
        |        |  clone: (Db*)(D1*)D1::clone
            |________|
       D1 part |        |
        |        |
            |________|

The reason for picking R1' to be the same as Ra' was that then D1 and Da
could still share the vtable, and no thunk needed to be introduced.  A thunk
is unavoidable for Db however.  Note the reason for the double cast:
D1::clone returns a Da* pointer to a D1 object, which is cast to
a D1* and then up the other branch of the inheritance DAG to a Db*; obviously
this can be optimized to just one pointer offset.

The cloning example above is modified by the compiler automatically to

class Da {
 Da* clone() { return new Da; }
};  // no change -- we don't know about contravariance yet

class Db {
 Db* clone() { return new Db; }
};  // no change -- we don't know about contravariance yet

class D1: public Da, public Db {
 Da* clone() { return (Da*)new D1; } // change return type to Da*
}
class D2: public D1 {
 Da* clone() { return (Da*)new D2; } // change return type to Da*

D1 d1;
Da* dap = &d1; Db* dbp = &d1; D1* d1p = &d1;

(D1*)(d1p->clone()); // a pointer to a D1 which is a copy of d1
  // d1p->clone() returns a ptr to the Da part of a D1
  // which is then cast to a D1* (adjusting the pointer value)
dap->clone();// a pointer to the Da part of a D1 which is a copy of d1
  // no change to this -- it could occur in a context where dap
  // pointed to a Da object and there was no knowledge of
  // D1 or contravariance; in this case D1::clone is invoked
  // which has been modified to return a Da*
dbp->clone();// a pointer to the Db part of a D1 which is a copy of d1
  // no change to this -- it could occur in a context where dbp
  // pointed to a Db object and there was no knowledge of
  // D1 or contravariance; in this case the thunk
  //  (Db*)(D*)(D::clone())
  // is invoked.  D::clone() returns a ptr to the Da part of a
  // D1 which gets cast in two steps to the Db part of the same
  // D1.

Similarly,

(D2*)(d2p->clone()); // a pointer to a D2 which is a copy of d2
(D1*)(d1p->clone()); // a pointer to the D1 part of a D2 which is a copy of d2
dap->clone();// a pointer to the Da part of a D2 which is a copy of d2
dbp->clone();// a pointer to the Db part of a D2 which is a copy of d2

EXAMPLE 3 -- virtual base classes

Virtual base classes would pose no additional complication if it were possible
to cast from a virtual base class pointer to an object back down to a pointer
to the object.  However, if B is a virtual base class of D, and B* b = new D,
it is illegal to apply the cast (D*)b, even though b does point to the B part
of a D object.  This is because it is not known at runtime where in the D
object the B part might be.  However, the implementation described above
still works at the cost of introducing some thunks.  Here is an example.


class D0 { D0* clone() { new D0; } };
class D1a: virtual public D0 {  D1a* clone() { new D1a; }};
class D1b: virtual public D0 {  D1b* clone() { new D1b; }};
class D2: public D1a, public D1b { D2* clone() { new D2; }};

D2 d2;
D0* d0p = &d2;
D1a* d1ap = &d2;
D1b* d1bp = &d2;
D2* = &d2;

d0p->clone(); // a pointer to the D0 part of a D2 (a copy of d2)
d1ap->clone(); // a pointer to the D1a part of a D2 (a copy of d2)
d1bp->clone(); // a pointer to the D1b part of a D2 (a copy of d2)
d2->clone(); // a pointer to a new D2 (copy of d2)

Following the rules above, R0' = R0 = D0, R1a' = R1a = D1a ( because D1a is
virtually-inherited from D0), R1b' = R1b = D1b, and R2' = R1a' = D1a.

Only the definition of D2::clone ends up being changed -- to have a return
type of D1a*.

The virtual function tables are modified like this:

D0 -- unchanged: the slot for clone in D0 contains &D0::clone()

      _______
D1a object: D1a part: |       |
     |   ----|-----vtable: clone:  &D1a::clone
     |       |
     |       |
     |_______|
  D0 part:  |       |
     |   ----|-----vtable: clone:  (D0*)(D1a*)D1a::clone
     |       |   [ vtables could have been shared in the
     |_______|    absence of contravariance]

D1b -- changed analogously to D1a

      _______
D2 object: D1a part: |       |
     |   ----|-----vtable: clone:  &D2::clone
     |       |
     |       |
     |_______|
  D1b part: |       |
     |   ----|-----vtable: clone:  (D1b*)(D2*)D2::clone
     |       |
     |_______|
  D2 part:  |       |
     |   ----|-----vtable: clone:  &D2::clone
     |       | [ Would be shared with D1a; could also
     |_______| use same structure element for the ptr]
  D0 part:  |       |
     |   ----|-----vtable: clone:  (D0*)(D2*)D2::clone
     |       |
     |_______|

 Because the modified return type of D2::clone was chosen to be the
 same as the modified return type of D1a::clone (namely D1a*), the
 vtable for D2 and the vtable for D1a can still be shared.

The lines of code become

d0p->clone(); // no change
  // on execution it calls the thunk (D0*)(D2*)D2::clone
  // D2::clone returns a D1a* which points to the D1a part of a
  // D2 object; this is cast to a pointer to the D0 part of the
  // D2 object
d1ap->clone(); // a pointer to the D1a part of a D2 (a copy of d2)
  // on execution it calls D2::clone which returns a D1a*
  // which is eventually cast to a D0*
d1bp->clone(); // a pointer to the D1b part of a D2 (a copy of d2)
  // on execution it calls the thunk (D1b*)(D2*)D2::clone() which
  // (after casting) is a pointer to the D1b part of the new D2
  // object
((D2*)(d2->clone())); // a pointer to a new D2 (copy of d2)
  // This calls D2::clone which returns a pointer to the D1a part
  // of a D2 object; this is cast to be a pointer to the D2 part
  // of the same object.

Note that there would not be any problem with virtual base classes among the
return types (beyond that caused by multiply-inheriting the contravariant
function) if it were possible to cast from the virtual base class to the
derived class.

CONCLUSION

-- There is a low-cost implementation of contravariance (lower cost than
   multiple inheritance or virtual functions themselves)
-- It does not add any cost to functions which are not contravariant
-- The implementation works in the context of separate compilation
-- For single inheritance, the execution time cost is only at most an extra
   add of a compile-time constant in the execution of a virtual function and
   an extra add of a compile-time constant at each location where such a
   function is used.
-- In some multiple inheritance cases, thunks must be introduced, but in most
   cases, virtual function tables and pointers to such tables which could be
   shared in the absence of contravariance, can still be shared.

The utility of contravariant functions -- such as for clone -- is high.
It simplifies programs and reduces the amount of casting a programmer needs to
do.  Another example I have encountered in real problems is providing numeric
operations for structured types: Base class NumericImage, derived classes
DoubleImage, FloatImage, ...; an operator-() should return a NumericImage&
when applied to a NumericImage, a DoubleImage& when applied to a DoubleImage,
etc.

What is preventing contravariance on function return type from being adopted?

David R. Cok
Imaging Science Lab
Eastman Kodak Company
cok@Kodak.COM