Can anyone tell me how does return type covariance work in the following code?
class X
{
public:
int x;
};
class Y: public OtherClass, public X
{
};
static Y inst;
class A {
public:
virtual X* out() = 0;
};
class B : public A
{
public:
virtual Y* out()
{
return &inst;
}
};
void main()
{
B b;
A* a = &b;
//x and y have different addresses. how and when is this conversion done??
Y* y = b.out();
X* x = a->out();
}
EDIT: I'm sorry I must not have been clear enough. x and y point to different addresses just like I expect since there's multiple inheritance involved so X and Y objects are not on the same address. My question is when is this cast being done? the out() function couldn't have done this because it always returns a pointer to Y from its point of view. The caller of out() couldn't have done this because it sees X* whose concrete type might be X or Y. When is the cast done then?
Covariant return type refers to return type of an overriding method. It allows to narrow down return type of an overridden method without any need to cast the type or check the return type. Covariant return type works only for non-primitive return types.
How is Covariant return types implemented? Java doesn't allow the return type-based overloading, but JVM always allows return type-based overloading. JVM uses the full signature of a method for lookup/resolution. Full signature means it includes return type in addition to argument types.
October 12, 2021. In an object-oriented programming language, return type covariance means that a method's return type can be replaced with a narrower one when overridden in a subclass or child class. It eliminates the need to cast or validate the return type when limiting the return type of a custom method.
Java allows for Covariant Return Types, which means you can vary your return type as long you are returning a subclass of your specified return type. Method Overriding allows a subclass to override the behavior of an existing superclass method and specify a return type that is some subclass of the original return type.
By "how does it work", I presume you're asking about what the generated code looks like. (In a typical implementation, of course. We all know that the generated code can vary somewhat between implementations.) I'm aware of two possible implementations:
The compiler always generates code to return a pointer to the base
class; i.e. in B::out
, the compiler will convert the Y*
to an X*
before returning it. At the call site, if the call was through an
lvalue with static type B
, the compiler will generate code to
reconvert the returned value to Y*
.
Alternatively (and I think this is more frequent, but I'm far from
sure), the compiler generates thunks, so when you call a->out
, the
virtual function which gets called is not directly B::out
, but a small
wrapper which converts the Y*
returned from B::out
to an X*
.
Both g++ and VC++ seem to use thunks (from a very quick glance).
When is the cast done then?
Well, it is done after B::out
returns, and before the A::out
call ends. There really isn't much to it.
The pointer returned by the polymorphic call must be of type X*
. This means it has to point to an object of type X
. Since you're using multiple inheritance, your Y
objects can have several subobjects in them. In your case they can have an OtherClass
subobject and an X
subobject. Obviously, these subobjects are not stored in the same address. When you ask for a pointer to X*
you get a pointer to the X
subobject and when you ask for a pointer to Y*
you get a pointer to the whole Y
object.
class OtherClass { int a; };
// the other classes
int main()
{
B b;
A* a = &b;
//x and y have different addresses. how and when is this conversion done??
Y* y = b.out();
X* x = a->out();
OtherClass* o = y;
std::cout << "x: " << x << std::endl;
std::cout << "y: " << y << std::endl;
std::cout << "o: " << o << std::endl;
}
You can see that running this code yields different addresses for the X*
and Y*
pointers and the same address for the Y*
and OtherClass*
pointers, because the object was laid out with the OtherClass
subobject before the X
subobject.
x: 0x804a15c
y: 0x804a158
o: 0x804a158
NOTE: first answer is only correct for single inheritance, multiple inheritance below (edit2).
it's normal for x and y to have different addresses because it are two different pointers. They do have the same value though, which is the adress of the variable they're pointing to.
edit: you can use this main to check what I mean, the first line will print the value of x
and y
(i.e. the address they're pointing to) which always should be the same because actually the same method will be called due to out
being virtual. The second will print their own address, which are of course different because they're different (pointer) variables.
#include <iostream>
int main()
{
B b;
A* a = &b;
//x and y have different addresses. how and when is this conversion done??
Y* y = b.out();
X* x = a->out();
std::cout << y << std::endl << x << std::endl;
std::cout << &y << std::endl << &x << std::endl;
return 0;
}
edit2: okay, this was wrong, my apologies. When multiple inheritance comes into play the implicit cast from Y*
to X*
(so at the assignment of x, not the return of out
) will change the pointer address.
This happens because at implementation, the layout of Y
contains 2 additional class implementations, being the part it inherited from OtherClass
and the part it inherited from X
. When implicitly casting to X*
(which is allowed) the address has to change of course to point to the X
-part of Y
as X does not know of OtherClass
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With