views:

264

answers:

6

C++ and Java support return-type covariance when overriding methods.

Neither, however, support contra-variance in parameter types - instead, it translates to overloading (Java) or hiding (C++).

Why is that? It seems to me that there is no harm in allowing that. I can find one reason for it in Java - since it has the "choose-the-most-specific-version" mechanism for overloading anyway - but can't think of any reason for C++.

Example (Java):

class A {
    public void f(String s) {...}
}
class B extends A {
    public void f(Object o) {...} // Why doesn't this override A.f?
}
+5  A: 

For C++, Stroustrup discusses the reasons for hiding briefly in section 3.5.3 of The Design & Evolution of C++. His reasoning is (I paraphrase) that other solutions raise just as many issues, and it has been this way since C With Classes days.

As an example, he gives two classes - and a derived class B. Both have a virtual copy() function which takes a pointer of their respective types. If we say:

A a;
B b;
b.copy( & a );

that is currently an error, as B's copy() hides A's. If it were not an error, only the A parts of B could be updated by A's copy() function.

Once again, I've paraphrased - if you are interested, read the book, which is excellent.

anon
Do you have an example to an issue this raises?
Oak
This doesn't sound like contravariance to me. If we have `void A::copy(A*)` and `void B::copy(B*)`, and if we have that `B <: A`, then contravariance would be if we had `void B::copy(A*)` and `void A::copy(B*)`. You have covariance, which is permitted for return types but is type-unsafe for argument types. (Although I might have gotten it backwards—I have a tendency to flip these around. But I'm fairly sure.)
Antal S-Z
@Antal The OP's question specifically mentioned hiding in C++, and the Java code in his example is covariant.
anon
@Neil: I could be wrong, but I looked it up and the OP's example is contravariant. If it was covariant, then it would be a form of multiple dispatch (see http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)). That was my mixup, so I deleted my answer.
stefaanv
@stef I agree with you, it is contra-variant.
FredOverflow
Neil: in Oak's example, the subclass's method takes an argument of a *more* general type: `B <: A` and `Object :> String`. In your example, the subclass's method takes an argument of a *less* general type: `B <: A` and `B <: A`.
Antal S-Z
This is an argument on why in C++ the method in the derived class hides the method in the base class (as compared to Java pulling the base method into the derived class), but it is unrelated to contra-variance which is the main question. Still, +1 for the contents.
David Rodríguez - dribeas
+2  A: 

Although this is a nice-to-have in any oo language, I still need to encounter it's applicability in my current job.

Maybe there isn't really a need for it.

xtofl
+3  A: 
class A {
    public void f(String s) {...}
    public void f(Integer i) {...}
}

class B extends A {
    public void f(Object o) {...} // Which A.f should this override?
}
Don Roby
Good point, though I can see no reason it cannot override both, since every `f` call can legally be redirected to `B.f`.
Oak
+3  A: 

Thanks to Donroby for his answer above - I'm just extending on it.

interface Alpha
interface Beta
interface Gamma extends Alpha, Beta
class A {
    public void f(Alpha a)
    public void f(Beta b)
}
class B extends A {
    public void f(Object o) {
        super.f(o); // What happens when o implements Gamma?
    }
}

You're falling on a problem akin to the reason multiple implementation inheritance is discouraged. (If you try to invoke A.f(g) directly, you'll get a compile error.)

jhominal
But `super.f(o)` should raise a compile-time type error: it can be statically known that there is no `super.f` which takes an `Object`. (What would happen if you called `new B().f("bad")`, for instance? It's not merely ambiguous, but type-unsafe!) There are a large class of type-unsafe programs which arise if you allow unrestricted use of `super`, but you can have parameter contravariance without this.
Antal S-Z
Well, I thought that if we want to build parameter contra-variance, there would have to be a specification for `super` (but as it stands, there is no specification that could be consistent and useful).Then, let's disallow `super` completely in B.f(Object). What, then, is the point of the feature, except adding yet one more method linking rule to C++?
jhominal
+5  A: 

On the pure issue of contra-variance

Adding contra-variance to a language opens a whole lot of potential problems or unclean solutions and offers very little advantage as it can be easily simulated without language support:

struct A {};
struct B : A {};
struct C {
   virtual void f( B& );
};
struct D : C {
   virtual void f( A& );     // this would be contravariance, but not supported
   virtual void f( B& b ) {  // [0] manually dispatch and simulate contravariance
      D::f( static_cast<A&>(b) );
   }
};

With a simple extra jump you can manually overcome the problem of a language that does not support contra-variance. In the example, f( A& ) does not need to be virtual, and the call is fully qualified to inhibit the virtual dispatch mechanism.

This approach shows one of the first problems that arise when adding contra-variance to a language that does not have full dynamic dispatch:

// assuming that contravariance was supported:
struct P {
   virtual f( B& ); 
};
struct Q : P {
   virtual f( A& );
};
struct R : Q {
   virtual f( ??? & );
};

With contravariance in effect, Q::f would be an override of P::f, and that would be fine as for every object o that can be an argument of P::f, that same object is a valid argument to Q::f. Now, by adding an extra level to the hierarchy we end up with design problem: is R::f(B&) a valid override of P::f or should it be R::f(A&)?

Without contravariance R::f( B& ) is clearly an override of P::f, since the signature is a perfect match. Once you add contravariance to the intermediate level the problem is that there are arguments that are valid at the Q level but are not at either P or R levels. For R to fulfill the Q requirements, the only choice is forcing the signature to be R::f( A& ), so that the following code can compile:

int main() {
   A a; R r;
   Q & q = r;
   q.f(a);
}

At the same time, there is nothing in the language inhibiting the following code:

struct R : Q {
   void f( B& );    // override of Q::f, which is an override of P::f
   virtual f( A& ); // I can add this
};

Now we have a funny effect:

int main() {
  R r;
  P & p = r;
  B b;
  r.f( b ); // [1] calls R::f( B& )
  p.f( b ); // [2] calls R::f( A& )
}

In [1], there is a direct call to a member method of R. Since r is a local object and not a reference or pointer, there is no dynamic dispatch mechanism in place and the best match is R::f( B& ). At the same time, in [2] the call is made through a reference to the base class, and the virtual dispatch mechanism kicks in.

Since R::f( A& ) is the override of Q::f( A& ) which in turn is the override of P::f( B& ), the compiler should call R::f( A& ). While this can be perfectly defined in the language, it might be surprising to find out that the two almost exact calls [1] and [2] actually call different methods, and that in [2] the system would call a not best match of the arguments.

Of course, it can be argued differently: R::f( B& ) should be the correct override, and not R::f( A& ). The problem in this case is:

int main() {
   A a; R r;
   Q & q = r;
   q.f( a );  // should this compile? what should it do?
}

If you check the Q class, the previous code is perfectly correct: Q::f takes an A& as argument. The compiler has no reason to complain about that code. But the problem is that under this last assumption R::f takes a B& and not an A& as argument! The actual override that would be in place would not be able to handle the a argument, even if the signature of the method at the place of call seems perfectly correct. This path leads us to determine that the second path is much worse than the first. R::f( B& ) cannot possibly be an override of Q::f( A& ).

Following the principle of least surprise, it is much simpler both for the compiler implementor and the programmer not to have contra variance in function arguments. Not because it is not feasible, but because there would be quirks and surprises in code, and considering that there are simple work-arounds if the feature is not present in the language.

On Overloading vs Hiding

Both in Java and C++, in the first example (with A, B, C and D) removing the manual dispatch [0], C::f and D::f are different signatures and not overrides. In both cases they are actually overloads of the same function name with the slight difference that because of the C++ lookup rules, the C::f overload will by hidden by D::f. But that only means that the compiler will not find the hidden overload by default, not that it is not present:

int main() {
   D d; B b;
   d.f( b );    // D::f( A& )
   d.C::f( b ); // C::f( B& )
}

And with a slight change in the class definition it can be made to work exactly the same as in Java:

struct D : C {
   using C::f;           // Bring all overloads of `f` in `C` into scope here
   virtual void f( A& );
};
int main() {
   D d; B b;
   d.f( b );  // C::f( B& ) since it is a better match than D::f( A& )
}
David Rodríguez - dribeas
Thanks, that's precisely the sort of contravariance-induced issues I was looking for.
Oak
David Rodríguez - dribeas
A: 

Thanks to donroby's and David's answers, I think I understand that the main problem with introducing parameter contra-variance is the integration with the overloading mechanism.

So not only is there a problem with a single override for multiple methods, but also the other way:

class A {
    public void f(String s) {...}
}

class B extends A {
    public void f(String s) {...} // this can override A.f
    public void f(Object o) {...} // with contra-variance, so can this!
}

And now there are two valid overrides for the same method:

A a = new B();
a.f(); // which f is called?

Other than the overloading issues, I couldn't think of anything else.

Oak
It could go for the most specific one (string, in this case).
devoured elysium