What do covariant return types buy us?

B

Bob Hairgrove

Consider the classic clone() function:

class A {
public:
virtual ~A() {}
virtual A* clone() const = 0;
};

class B : public A {
public:
// overrides A::clone() due to covariant return:
B* clone() const { return new B(*this); }
};

// etc.

If I am storing the return value of clone() in a container of pointers
to A, this buys me nothing, because there is an implicit conversion
from B* to A*. Indeed, with compilers which do not support covariant
return types, I can do exactly the same by declaring the return type
of B::clone() as A*.

I'm sure there was a reason for allowing this, but I can't seem to
come up with a concrete example of how I need this to do something I
couldn't do on a compiler which doesn't implement it.

--
Bob Hairgrove
(e-mail address removed)

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
A

Alf P. Steinbach

* Bob Hairgrove:
Consider the classic clone() function:

class A {
public:
virtual ~A() {}
virtual A* clone() const = 0;
};

class B : public A {
public:
// overrides A::clone() due to covariant return:
B* clone() const { return new B(*this); }
};

// etc.

If I am storing the return value of clone() in a container of pointers
to A, this buys me nothing, because there is an implicit conversion
from B* to A*. Indeed, with compilers which do not support covariant
return types, I can do exactly the same by declaring the return type
of B::clone() as A*.

I'm sure there was a reason for allowing this, but I can't seem to
come up with a concrete example of how I need this to do something I
couldn't do on a compiler which doesn't implement it.

Consider that in B you need to (1) override A::clone, and (2) provide a
B-specific pointer to clients using B directly. If the compiler doesn't
support covariant types, that involves a downcast in B's code,
somewhere. Covariance saves you from that, admittedly trivial and easy
to prove correct in each concrete case, downcast.

On the other hand, covariance only saves you from the downcast when the
result type is a raw pointer or reference; for a smart pointer result
you're not saved, but have to Do It Yourself.

To make smart pointers work more like Java/C# references we'd need --
uh, something. It's late in my day. But I remember thinking about it
and that's where the crux of the matter is located, roughly.


--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
P

Pete Becker

Bob said:
I'm sure there was a reason for allowing this, but I can't seem to
come up with a concrete example of how I need this to do something I
couldn't do on a compiler which doesn't implement it.

When you know you've got a pointer to B, you can get another pointer to B:

B *ptr1 = new B;
B *ptr2 = ptr1->clone();

--

Pete Becker
Roundhouse Consulting, Ltd.

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
G

Greg Herlihy

Bob said:
Consider the classic clone() function:

class A {
public:
virtual ~A() {}
virtual A* clone() const = 0;
};

class B : public A {
public:
// overrides A::clone() due to covariant return:
B* clone() const { return new B(*this); }
};

// etc.

If I am storing the return value of clone() in a container of pointers
to A, this buys me nothing, because there is an implicit conversion
from B* to A*. Indeed, with compilers which do not support covariant
return types, I can do exactly the same by declaring the return type
of B::clone() as A*.

I'm sure there was a reason for allowing this, but I can't seem to
come up with a concrete example of how I need this to do something I
couldn't do on a compiler which doesn't implement it.

Consider the case in which you have a container of B pointers which you
wish to populate by cloning some class B objects. Without a covariant
return type for B's clone() method, you would not be able to put the B
clones' pointers in the container; because the class A pointers
obtained from cloning the B class objects are not the right type.

And about the only way around this problem would be to "downcast" the A
pointers. But downcasting is an unattractive solution. Downcasts in
general are widely viewed as a poor programming practice, but worse - a
downcast is not always guaranteed to work. And while downcasting the
returned pointer of a "clone" routine may seem safe and harmless, what
about the pointers returned by the class' other virtual methods? For
which can the returned pointer be downcast, and for which can it not?

Without covariant return types, the answer is that the client simply
has to "know" when a method returns a pointer to a subclass of the one
declared - should the client have need for that information. Because
the problem here is that the method's declaration is only partially
correct - to get the complete information, requires some kind of
special arrangement be made. This arrangement lets a client ignore a
method's declared return type and be able to be certain that a pointer
to a subclassed object will be returned instead.

But it is precisely these kinds of side arrangements - in which needed
information is to be found somewhere outside of the declared interface
- that make software programs difficult to maintain and - almost always
- eventually cause something to break. So covariant return types simply
correct a deficit of information. With the return type accurately
advertised, a program no longer has to compensate for the missing
information by performing questionable downcasts or by learning the
particulars of an interface's implementation.

Greg


[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
D

David Abrahams

Alf P. Steinbach said:
* Bob Hairgrove:

Consider that in B you need to (1) override A::clone, and (2) provide a
B-specific pointer to clients using B directly. If the compiler doesn't
support covariant types, that involves a downcast in B's code,
somewhere. Covariance saves you from that, admittedly trivial and easy
to prove correct in each concrete case, downcast.

You don't even need a downcast.

class B : public A
{
public:
B* cloneme() const { return new B(*this); }
private
A* clone() const { return this->cloneme(); }
}

And of course you can use CRTP to do a single downcast for the whole
program. Covariant return types is a very trivial bit of syntactic
sugar.

--
Dave Abrahams
Boost Consulting
www.boost-consulting.com

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
J

James Slaughter

David said:
And of course you can use CRTP to do a single downcast for the whole
program. Covariant return types is a very trivial bit of syntactic
sugar.

I wouldn't say they're /entirely/ trivial:

struct a { virtual a* clone() const = 0; };
struct b { virtual b* clone() const = 0; };

struct ab: a, b
{
virtual ab* clone() const { return new ab(*this); }
};

int main()
{
delete ab().clone();
}

Without covariant return types, one would need to introduce intermediate
classes to 'rename' the functions of the base classes (one of them, at
least). CRTP can't do that very well because we can't currently forward
the constructors (very well).

(Of course, covariant return types aren't generally applicable where
two base classes have member functions with the same signature.)

Regards,
James.

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
W

werasm

Bob said:
I'm sure there was a reason for allowing this, but I can't seem to
come up with a concrete example of how I need this to do something I
couldn't do on a compiler which doesn't implement it.

This may be an example:

struct Interface
{
virtual void do_1() = 0;
Interface* clone() const = 0;
};

struct Extended : Interface
{
using Interface::do_1;
virtual void do_2() = 0;
Extended* clone() const = 0; //covariant!
};

Now :

class Client1
{
//...
Interface* impl_;
};

Without changing Client1, we can now extend the interface for use with
Client2, that requires an additional service (which is also abstract):

class Client2
{
//...
Extended* impl_;
};

If we did not have covariant return types, on creation of Client2
Interface would need to be downcasted to Extended. Now we don't need
this anymore.

Hierarchy is therefore:

Interface <--- Implementation
Interface <--- Extended <--- ExtendedImplementation

Regards,

Werner

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
W

werasm

Bob said:
I'm sure there was a reason for allowing this, but I can't seem to
come up with a concrete example of how I need this to do something I
couldn't do on a compiler which doesn't implement it.

Consider this:

struct Interface
{
virtual void do_1() = 0;
virtual Interface* clone() const = 0;
};

struct Extended : Interface
{
using Interface::do_1;
virtual void do_2() = 0;
virtual Extended* clone() const = 0;
};

Note that <Extended> is still abstract, and may be required by clients
as an interface. Therefore the clients using the extended interface
would want to use <Extended> as prototype, and not <Interface>. On the
other hand, clients already using <Interface> as interface, would
remain unchanged.

The hierarchy becomes:

Interface <-- Impl
Interface <-- Extended <-- ExtendedImpl

Covariance makes the possible without glitches.

Kind regards,

Werner

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
A

Alf P. Steinbach

* David Abrahams:
You don't even need a downcast.

class B : public A
{
public:
B* cloneme() const { return new B(*this); }
private

Missing colon.
A* clone() const { return this->cloneme(); }
}

Missing semicolon -- so I think this was written in a hurry.

You do need a downcast if you want the same public interface for the
cloning operation in B, as in A, without language support.

In your example the publicly available cloning operation is 'cloneme' in
B, and something else in A; your class A might look like

class A
{
public:
A* cloneme_as_A() const { return new A(*this); }
private:
virtual A* clone() const { return cloneme_as_A(); }
};

Unless you were thinking of 'cloneme' as a virtual member function, in
which case you were both assuming language support for covariance and
lack of such support, or unless you were thinking only of
non-polymorphic calls, in which case this machinery is unnecessary.

The ordinary clone operation is a pulic, /virtual/ member function.

And of course you can use CRTP to do a single downcast for the whole
program.

Also that at a cost -- have you tried it?

Covariant return types is a very trivial bit of syntactic sugar.

Nah, there are subtleties -- see above... ;-)

--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
A

Alf P. Steinbach

* Alf P. Steinbach:
* David Abrahams:
>> [snip, essentially that you need a downcast]
You don't even need a downcast.
[snip]

Nah, there are subtleties -- see above... ;-)

There are, but I posted on an empty stomach (now corrected).

What I should have written was simply that your code, under the
assumption of no covariance support in the language,

class B : public A
{
public:
B* cloneme() const { return new B(*this); }
private:
A* clone() const { return this->cloneme(); }
};

does not provide a polymorphic clone operation where a B can be cloned
via an A pointer or reference.

Hence, "you don't even need a downcast" is not proven by this code; this
code is irrelevant.

--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
D

David Abrahams

Alf P. Steinbach said:
Missing colon.


Missing semicolon -- so I think this was written in a hurry.

Of course it was.
You do need a downcast if you want the same public interface for the
cloning operation in B, as in A,

Why is that important to have?
without language support. In your example the publicly available
cloning operation is 'cloneme' in B, and something else in A; your
class A might look like

class A
{
public:
A* cloneme_as_A() const { return new A(*this); }
private:
virtual A* clone() const { return cloneme_as_A(); }
};

Unless you were thinking of 'cloneme' as a virtual member function,
Nope.


Also that at a cost -- have you tried it?

I have to admit, I haven't. It looks like you have to solve the
forwarding problem (again).
Nah, there are subtleties -- see above... ;-)

I see. Why have I never needed it? Who does need it?

--
Dave Abrahams
Boost Consulting
www.boost-consulting.com

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
D

David Abrahams

Alf P. Steinbach said:
* Alf P. Steinbach:
* David Abrahams:
[snip, essentially that you need a downcast]
You don't even need a downcast.
[snip]

Nah, there are subtleties -- see above... ;-)

There are, but I posted on an empty stomach (now corrected).

I think you were better on an empty stomach. Your other points are
well taken (after some thought to figure out what you meant).
What I should have written was simply that your code, under the
assumption of no covariance support in the language,

class B : public A
{
public:
B* cloneme() const { return new B(*this); }
private:
A* clone() const { return this->cloneme(); }
};

does not provide a polymorphic clone operation where a B can be cloned
via an A pointer or reference.

Huh?

class A { virtual A* clone() const = 0; };

B's clone overrides A's clone and calls B's cloneme. What am I
missing this time?

--
Dave Abrahams
Boost Consulting
www.boost-consulting.com

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
A

Alf P. Steinbach

* David Abrahams:
Alf P. Steinbach said:
* Alf P. Steinbach:
* David Abrahams:
[snip, essentially that you need a downcast]
You don't even need a downcast. [snip]
Nah, there are subtleties -- see above... ;-)
There are, but I posted on an empty stomach (now corrected).

I think you were better on an empty stomach. Your other points are
well taken (after some thought to figure out what you meant).
What I should have written was simply that your code, under the
assumption of no covariance support in the language,

class B : public A
{
public:
B* cloneme() const { return new B(*this); }
private:
A* clone() const { return this->cloneme(); }
};

does not provide a polymorphic clone operation where a B can be cloned
via an A pointer or reference.

Huh?

class A { virtual A* clone() const = 0; };

B's clone overrides A's clone and calls B's cloneme. What am I
missing this time?

Don't know whether you're missing something or just have a different
perspective and/or assumptions.

Let's say, in external client code, you have a B* pointer:

B* b1 = ...;

You want to clone the object:

B* b2 = b1->cloneme();

What if the dynamic type of *b1 is really class C, derived from B?

You get a B slice of that object.

With a downcast, calling clone() from cloneme() instead of opposite,
where cloneme() does the downcast, you can avoid that slicing.

And C++ covariance saves you from doing the downcast, except when the
result is a smart-pointer... ;-)

--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
D

David Abrahams

Alf P. Steinbach said:
Don't know whether you're missing something or just have a different
perspective and/or assumptions.

Let's say, in external client code, you have a B* pointer:

B* b1 = ...;

You want to clone the object:

B* b2 = b1->cloneme();

What if the dynamic type of *b1 is really class C, derived from B?

You get a B slice of that object.

Oh, I got all that long ago. What I'm taking issue with here is your
assertion that my B declaration "does not provide a polymorphic clone
operation where a B can be cloned via an A pointer or reference."
That seems clearly false.


--
Dave Abrahams
Boost Consulting
www.boost-consulting.com

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
A

Alf P. Steinbach

* David Abrahams:
Oh, I got all that long ago. What I'm taking issue with here is your
assertion that my B declaration "does not provide a polymorphic clone
operation where a B can be cloned via an A pointer or reference."
That seems clearly false.

Huh?

Just add "in general".

Obviously one may use the private virtual clone() operation to clone
from /within/ class B or descendants. And just as obviously, that
involves a downcast of the result, if static type B is needed.

But the code was introduced with the statement "You don't even need a
downcast", blank line, code, and was presumably meant to exemplify that;
no downcast needed in the case of no covariance support in the language.

Am I missing something?

--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
D

David Abrahams

Alf P. Steinbach said:
Huh?

Just add "in general".

Obviously one may use the private virtual clone() operation to clone
from /within/ class B or descendants. And just as obviously, that
involves a downcast of the result, if static type B is needed.

But the code was introduced with the statement "You don't even need a
downcast", blank line, code, and was presumably meant to exemplify that;
no downcast needed in the case of no covariance support in the language.

Am I missing something?

This is getting silly. Your assertion was nothing to do with anything
"/within/ B or descendants," but something about starting with an A
pointer or reference and not being able to clone the B it refers to.
If you give me p, a B* cast to an A*, I can write p->clone() and get a
new B object.

If you still don't understand, then let's just drop it, because,
_really_ it's unimportant.

--
Dave Abrahams
Boost Consulting
www.boost-consulting.com

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 
A

Alf P. Steinbach

* David Abrahams:
This is getting silly. Your assertion was nothing to do with anything
"/within/ B or descendants," but something about starting with an A
pointer or reference and not being able to clone the B it refers to.
If you give me p, a B* cast to an A*, I can write p->clone() and get a
new B object.

Can you?

// Your classes A and B verbatim, only syntax corrected.
class A { virtual A* clone() const = 0; };

class B : public A
{
public:
B* cloneme() const { return new B(*this); }
private:
A* clone() const { return this->cloneme(); }
};

int main()
{
A* p = new B;
A* p2 = p->clone();
}

The compiler complains:

(14): error C2248: 'A::clone' : cannot access private member declared
in class 'A'

Ah, well, that can be fixed by making 'clone()' public, then the above
compiles! :)



But then the abstract 'clone' has become the essential public interface,
instead of or in addition to the type-specific 'cloneme' which was all
the point; it's a very different public interface, a virtual function.

So, I'll assume you now mean clone() to be in the public interface.

If you still don't understand, then let's just drop it, because,
_really_ it's unimportant.

Yes and no. Maybe. I think it's important to do this the right way
around (the type-specific cloneme() calling the abstract clone(), and
downcasting), to avoid ugly surprises such as slicing.

If I can convince you of that more sound design, then these posting were
well worth writing.

Just to sum up, you offered a public, potentially slicing operation
cloneme() to try to demonstrate that downcasting isn't needed to emulate
C++ covariance.

And I say that that potential slicing demonstrates the opposite, and
should be avoided.

OK? ;-)

--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,982
Messages
2,570,190
Members
46,736
Latest member
zacharyharris

Latest Threads

Top