[ Send an empty e-mail to c++-...@netlab.cs.rpi.edu for info ]
[ about comp.lang.c++.moderated. First time posters: do this! ]
CPlusPlusGuy wrote:
>
> What exactly is a covariant return type and how is it useful? A very
> simplist example would be great. Thanks you.
>
What is "covariant". I cannot find this in the C++ language
reference, standard library, or the STL.
--
sma at sohnen-moe dot com
In essence, it means that a derived class member can return a derived class
reference or pointer from a member function.
Since code says more than a thousand words:
class Base
{
public:
virtual Base * clone () const;
};
class Derived : public Base
{
public:
virtual Derived * clone () const;
// without covariant return types, you'd have to write
// virtual Base * clone () const;
};
why is this useful? with covariant return types we can now write
Derived * d = new Derived ();
Derived * e = d->Clone ();
instead of introducing an ugly cast
Derived * d = new Derived ();
Derived * e = (Derived*) d->Clone ();
For more info about the background behind this, try searching for "Liskov
Subsitution Principle" on Google :)
How about:
class B
{
virtual B * Clone() {return new B(*this); }
};
class D: public B
{
virtual D * Clone() {return new D(*this); }
};
Without properly implemented covaiant return you can only return B* in all
overrides of the virtual function, what is quite a pain when you're in a
situation sing only D's around, never one B (which is probably abstract
anyway).
Paul
Base *d = new Derived;
Derived *e = d->clone();
In this case, is the covariant return pointer equivalent to a Derived * in
all ways? In particular, suppose class Derived had a _nonvirtual_ member
function:
int Derived::arbitraryfxn(){ /* ... definition ... */}
With the amended preceding code, would the following statement be legal?:
d->clone()->arbitraryfxn();
I ask this because it looks like the return type of an expression is being
determined at runtime, which seems like a great burden to place on a
compiler.
_______________
Hiram Berry
> "Burkhard Kloss" <b...@xk7.com> wrote in message
> news:96964227...@master.nyc.kbcfp.com...
> >
> > why is this useful? with covariant return types we can now write
> >
> > Derived * d = new Derived ();
> > Derived * e = d->Clone ();
>
> Wouldn't it be more useful to be able to write:
>
> Base *d = new Derived;
> Derived *e = d->clone();
This wouldn't compile. The static type of d id Base*, and Base::clone()
returns a Base*. Therefore, even though the dynamic type of d->clone() is
Derived*, the compiler won't implicitly downcast its static type (Base*) to
the type of e. You'd have to write:
Derived *e = static_cast<Derived*> (d->clone();
(or use dynamic_cast for safety).
> In this case, is the covariant return pointer equivalent to a Derived * in
> all ways?
No. In the code you've written above, the covariant return pointer has the
wrong static type.
> In particular, suppose class Derived had a _nonvirtual_ member
> function:
>
> int Derived::arbitraryfxn(){ /* ... definition ... */}
>
> With the amended preceding code, would the following statement be legal?:
>
> d->clone()->arbitraryfxn();
You've just illustrated why the static type is so important. No, the above
code would not be legal. While d->clone() has a dynamic type that would
permit invoking arbitraryfxn(), it has a static type that does not permit
it.
Change the declaration back to the original:
Derived *d = new Derived;
and then:
d->clone()->arbitraryfxn();
compiles just fine. It's to make this kind of statement work, without the
need for explicit casts, that covariant return types were introduced.
> I ask this because it looks like the return type of an expression is being
> determined at runtime, which seems like a great burden to place on a
> compiler.
Because of multiple inheritance, conversion between Derived* and Base* may
require some run-time adjustment (under the hood, of course, where the
programmer doesn't need to see it). In particular, when you call a virtual
method, the implicit this* may have to be adjusted at run time to convert
it, from the Base* that was used to invoke the method, to the Derived* that
will be used for the implicit 'this' pointer within the procedure body.
The amount of adjustment isn't known at compile time, because the compiler
doesn't know what the actual dynamic type will be. The usual way to handle
this is for the entry in the vtable to specify not only the address of the
actual code to use, but also the delta needed to convert the Base* to the
correct type.
All that's required to support covariant return types is for the vtable
entry to also specify the delta from the actual return type to the return
type declared in the Base version of the virtual function.
The overhead is that all the vtables need to be bigger (one extra word for
each virtual function), and one or two extra additions during virtual
function dispatch to convert the return type. A clever compiler could pay
this cost only where it might matter -- that is, only for virtual functions
that return a pointer or referent to a class type (because it's only these
return types that can be covariant).
That doesn't quite meet the "you only pay for it if you use it" criterion,
but it's not exactly onerous. It's right in line with the adjustment of the
'this' parameter on virtual function call, which you might pay for even if
you don't use multiple inheritance.
Another compiler strategy is to generate a wrapper function for each
virtual function override where 'this' and/or the return value actually
needs to be adjusted. In this case, you'd only pay a runtime cost when
multiple inheritance is actually used in a way that affects the result.
That is, if you have:
struct Base {
virtual Base* clone() const;
/*...*/ };
struct Derived: public OtherBase, public Base {
virtual Derived* clone() const;
/*...*/ };
the compiler would generate a glue function that looked like:
// All pointer conversions and 'this' parameters have been made
// explicit in the following
Base* Derived_Base_clone (const Base* const this) {
const Derived* aDerived = static_cast<const Derived*> (this);
Derived* result = Derived::clone(aDerived);
return static_cast<Base*> result; }
and make the vtable for a Derived point to this function for its
implementation of clone(). If (and only if) both of the static_casts in the
above are runtime no-ops, then the glue function doesn't need to be
generated, and the vtable can point directly to Derived::clone();
This form does satisfy "you only pay for it if you use it".
-Ron Hunsinger
Ah, now I see. Thanks, Ron for your explanation. That must be what the
standard means when it states "When the overriding function is called as the
final overrider of the overridden function, its result is converted to the
type returned by the (statically chosen) overridden function". So if d has
static type Derived * , d->clone() will return an actual Derived *, while if
d has static type Base * and dynamic type Derived * , the return of
d->clone() will be statically Base * and dynamically Derived * too.
> You'd have to write:
>
> Derived *e = static_cast<Derived*> (d->clone();
>
> (or use dynamic_cast for safety).
Yeah, since we're talking about a class hierarchy with virtual member
functions, dynamic_cast looks appropriate here. But this brings up an
interesting point. If polymorphism is really being exploited, usually we
want to use pointers to the base class. Yet covariance isn't needed in that
case:
Base *e = d->clone();
works, with e having dynamic type Derived *, whether covariance is
supported or not. OTOH, if you write
Derived * d = new Derived;
Derived * e = d->clone();
then there wasn't any need for clone() to have been virtual in the first
place; if nonvirtual Derived::clone() would have just hidden Base::clone().
So it looks to me that if you are using polymorphic pointers, covariance
doesn't give you any new functionality, while if the pointers aren't being
used polymorphically, the member functions involved needn't be virtual
anyway. I have to conclude then that covariance provides nothing more than
a shorthand mechanism for avoiding a dynamic_cast, ie. it provides no new
functionality.
>
> > In this case, is the covariant return pointer equivalent to a Derived *
in
> > all ways?
>
> No. In the code you've written above, the covariant return pointer has the
> wrong static type.
>
Right. What confused me was the assertion by several posters in this thread
and its mirror in c.l.c++ that covariance was essentially returning a
derived class pointer or reference in an overridden function. As it turns
out that isn't really a good way of thinking of it because of the issue of
the static type of the caller that you raised. Thanks for your explanation.
_____________
Hiram Berry
> >
> > > In this case, is the covariant return pointer equivalent to a
> > > Derived *
> in
> > > all ways?
> >
> > No. In the code you've written above, the covariant return pointer
has the
> > wrong static type.
> >
> Right. What confused me was the assertion by several posters in this
> thread
> and its mirror in c.l.c++ that covariance was essentially returning a
> derived class pointer or reference in an overridden function. As it
> turns
> out that isn't really a good way of thinking of it because of the
> issue of
> the static type of the caller that you raised. Thanks for your
> explanation.
Well, actually that's what it is. Here's how the standard defines it:
"If a function D::f overrides a function B::f, the return types of the
functions are covariant if they satisfy the following criteria:
- both are pointers to classes or references to classes
- the class in the return type of B::f is the same class as the class in
the return type of D::f or, is an unambiguous direct or indirect base
class of the class in the return type of D::f and is accessible in D
- both pointers or references have the same cv-qualification and the
class type in the return type of D::f has the same cv-qualification as
or less cv-qualification than the class type in the return type of
B::f."
So the assertion is correct - covariant return types means that a
derived class returns a pointer/reference to a class derived from the
base class's return type.
Note, by the way, that covariant return types can apply to parallel
heirarchies, as in:
class X
{
};
class A // NOTE: NOT derived from X
{
public:
X * f();
};
////////////////////////////
class Y : public X
{
};
class B : public A
{
public:
Y * f(); // OK, Y is derived from X
};
--
Jim
This message was posted using plain text only. Any hyperlinks you may
see were added by other parties without my permission.
I do not endorse any products or services that may be hyperlinked to
this message.
Sent via Deja.com http://www.deja.com/
Before you buy.
> But this brings up an
> interesting point. If polymorphism is really being exploited, usually we
> want to use pointers to the base class. Yet covariance isn't needed in that
> case:
>
> Base *e = d->clone();
>
> works, with e having dynamic type Derived *, whether covariance is
> supported or not. OTOH, if you write
>
> Derived * d = new Derived;
> Derived * e = d->clone();
>
> then there wasn't any need for clone() to have been virtual in the first
> place; if nonvirtual Derived::clone() would have just hidden Base::clone().
> So it looks to me that if you are using polymorphic pointers, covariance
> doesn't give you any new functionality, while if the pointers aren't being
> used polymorphically, the member functions involved needn't be virtual
> anyway. I have to conclude then that covariance provides nothing more than
> a shorthand mechanism for avoiding a dynamic_cast, ie. it provides no new
> functionality.
You're overlooking that the inheritance heirarchy can be deeper than we've
shown in the examples:
struct Vehicle {
virtual Vehicle* clone() const;
virtual ~Vehicle() {};
/* ... */ };
struct LandVehicle : public Vehicle {
/*virtual*/ LandVehicle* clone() const;
virtual int count_wheels() const = 0;
/* ... */ };
struct Car : public LandVehicle {
/*virtual*/ Car* clone() const;
/*virtual*/ int count_wheels() const { return 4; }
/* ... */ };
struct Truck : public LandVehicle {
/*virtual*/ Truck* clone() const;
/*virtual*/ int count_wheels() const { return 18; }
/* ... */ };
typedef std::vector<LandVehicle*> Fleet;
void foo() {
Fleet fleet;
fleet.push_back (new Car);
fleet.push_back (new Truck);
fleet.push_back (new Car);
Fleet spareFleet;
Fleet::iterator fend = fleet.end();
for (Fleet::iterator i = fleet.begin(); i != fend; ++i) {
spareFleet.push_back ((*i)->clone()); }
fend = spareFleet.end();
for (Fleet::iterator spareFleet.begin(); i != fend; ++i) {
std::cout << (*i)->count_wheels() << " "; }
}
We have a virtual function clone() that is first introduced in class
Vehicle. Without covariant return types, all the overrides would have to
return the same result type, namely Vehicle*.
A Fleet is a vector of pointers to LandVehicles. We construct one fleet by
pushing (pointers to) newly created LandVehicles of various types. That's
OK, because all the various things we're creating are subclasses of
LandVehicle.
Then we decide to duplicate the fleet by calling clone for each of its
members. The compiler can tell that each of the pointers being cloned is
actually a pointer to some kind of LandVehicle. It can tell this
statically, because each of the pointers is being pulled from a
std::vector<LandVehicle*>.
And the resulting clone must also be, not merely a Vehicle*, but
specifically a LandVehicle*. The compiler can tell this statically by
looking at the return type for LandVehicle::clone(). If we didn't have
covariant return types, LandVehicle::clone would have to return the same
type as the function it overrides, that is, Vehicle*.
Because the static type of each cloned pointer is LandVehicle*, no special
casting is needed to push it into a std::Vector<LandVehicle*>. Without
that, we'd either have to cast each pointer, or put them into a
std::vector<Vehicle*>.
When we then iterate over the members of the second fleet, we can invoke
the virtual LandVehicle::count_wheels() function on each. We couldn't have
done that without casts if we had been forced to make the fleet a
std::vector<Vehicle*>, because Vehicles don't have a count_wheels method.
But when we invoke each of the count_wheels() methods, the true dynamic
type comes through. That is, polymorphism is still working. The above code
will print "4 18 4 " (and leak a whole bunch of memory, because we never
delete those pointers, but hey, who's gonna notice).
-Ron Hunsinger