Generalizing covariant return types

156 views
Skip to first unread message

Andy Prowl

unread,
Dec 23, 2014, 8:31:06 AM12/23/14
to std-pr...@isocpp.org
Currently, covariant return types are allowed under the restrictions placed by [class.virtual]/7. I think these restrictions are unnecessarily severe.

For instance, the following is legal:

struct X { };
struct Y : X { };

struct A
{
   
virtual void X* foo();
    virtual void X& bar();
};

struct B : A
{
   
virtual void Y* foo() override;
   
virtual void Y& bar() override;
};

However, replacing raw pointers by unique_ptr or shared_ptr is not allowed. This makes little sense to me.

Given that B derives from A, I think that a function "T B::foo()" should be allowed to override a function "U A::foo()" if and only if T is implicitly convertible to U. Derived-to-base conversion would be just one particular case of this rule.

What do you think?

Kind regards,

Andy


Daniel Gutson

unread,
Dec 23, 2014, 9:00:32 AM12/23/14
to std-pr...@isocpp.org
In a past post, I wanted to go further by providing some mechanism to allow conversion from const vector<Derived>& to const vector<Base>& (vector being an example). However. This somehow overlaps the domain of concepts and parametric polymorphism.
From: Andy Prowl <andy....@gmail.com>
Date: Tue, 23 Dec 2014 05:31:06 -0800 (PST)
Subject: [std-proposals] Generalizing covariant return types
--

---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

David Krauss

unread,
Dec 23, 2014, 9:29:21 AM12/23/14
to std-pr...@isocpp.org
On 2014–12–23, at 10:00 PM, Daniel Gutson <daniel...@gmail.com> wrote:

In a past post, I wanted to go further by providing some mechanism to allow conversion from const vector<Derived>& to const vector<Base>& (vector being an example). However. This somehow overlaps the domain of concepts and parametric polymorphism.

Converting to a vector<Base> by value I’d understand, but how could you implement such a reference not to be dangling?

Assuming Andy’s rule is based on allowing user-defined conversions, you could create your own vector derivative with that functionality, without extending the proposal further.

What do you think?

The idea has been floating around for a while. Are there open questions remaining? Has there been any formal write-up?

Jean-Marc Bourguet

unread,
Dec 23, 2014, 9:31:31 AM12/23/14
to std-pr...@isocpp.org
I see where you are coming from.  There are details that have to be considered to be sure that it is implementable with a correct semantic.  The issue I see is that with implementation technique I know of covariant return, not only T has to be implicitly convertible to U but from a U you need to be able to reconstruct the T.  Your example is compiled as

struct A
{
   
void X* foo() { return __foo(); }
   
void X& bar() { return __bar(); }
   
virtual X* __foo();
   
virtual X& __bar();
};

struct B : A
{
   
virtual X* __foo() override;
   
virtual X& __bar() override;
    Y
* foo() { return static_cast<Y*>(this->foo()); }
    X
& bar() { return static_cast<Y&>(this->bar()); }
};

implicit conversion don't guarantee this round trip.  You will at least to show that there is a way to compile with the semantic you want.

Yours

Daniel Gutson

unread,
Dec 23, 2014, 9:33:52 AM12/23/14
to std-pr...@isocpp.org
My bad, errata: I meant pointers:
Conversion from

const vector<Derived*>&
to
const vector<Base*>&

AFAIK C# has an equivalent facility.
From: David Krauss <pot...@gmail.com>
Date: Tue, 23 Dec 2014 22:29:12 +0800
Subject: Re: [std-proposals] Generalizing covariant return types

--

Daniel Gutson

unread,
Dec 23, 2014, 9:40:27 AM12/23/14
to std-pr...@isocpp.org
FWIW,
http://msdn.microsoft.com/en-us/library/ee207183.aspx

Of course it's completely far in terms of implementation, I just want to show what other languages (that I very much dislike) do. Additionally, it seems they only allow this for arrays rather than for generic containers.
I didn't go much further to explore feasibility of a library solution.
From: "Daniel Gutson" <daniel...@gmail.com>
Date: Tue, 23 Dec 2014 14:33:47 +0000
Subject: Re: [std-proposals] Generalizing covariant return types

My bad, errata: I meant pointers:
Conversion from

const vector<Derived*>&
to
const vector<Base*>&

AFAIK C# has an equivalent facility.
From: David Krauss <pot...@gmail.com>
Date: Tue, 23 Dec 2014 22:29:12 +0800
Subject: Re: [std-proposals] Generalizing covariant return types

--

Jean-Marc Bourguet

unread,
Dec 23, 2014, 9:50:45 AM12/23/14
to std-pr...@isocpp.org, daniel...@gmail.com


Le mardi 23 décembre 2014 15:33:52 UTC+1, dgutson a écrit :
My bad, errata: I meant pointers:
Conversion from

const vector<Derived*>&
to
const vector<Base*>&

AFAIK C# has an equivalent facility.

This won't work with multiple inheritance.  If Derived inherit from B1 and B2, you can't have vector<Derived*> be a valid representation for both vector<B1*> and vector<B2*>.  The conversion of Derived* to at least one of B1* or B2* apply an offset to the pointer.

Yours, 

David Krauss

unread,
Dec 23, 2014, 10:16:30 AM12/23/14
to std-pr...@isocpp.org

On 2014–12–23, at 10:31 PM, Jean-Marc Bourguet <jm.bo...@gmail.com> wrote:

Your example is compiled as

If I’m grokking this correctly, current implementations leave the reverse conversion to the callee to avoid extra trampoline subroutines. The caller knows the correct derived return type, and when it can obtain that from the base type, the virtual callee doesn’t need to specialize on each derived type.

The cost of the extra trampolines is an extra vtable entry and a function containing the implicit cast code, per level of covariant overriding. It sounds reasonable to me, in any case users can avoid paying for it by not using it. Anyway, I don’t think that executing two implicit conversion functions is acceptable as a user-visible semantic.

Andy Prowl

unread,
Dec 23, 2014, 10:19:11 AM12/23/14
to std-pr...@isocpp.org
On Tuesday, December 23, 2014 3:31:31 PM UTC+1, Jean-Marc Bourguet wrote

I see where you are coming from.  There are details that have to be considered to be sure that it is implementable with a correct semantic.  The issue I see is that with implementation technique I know of covariant return, not only T has to be implicitly convertible to U but from a U you need to be able to reconstruct the T.  Your example is compiled as

struct A
{
   
void X* foo() { return __foo(); }
   
void X& bar() { return __bar(); }
   
virtual X* __foo();
   
virtual X& __bar();
};

struct B : A
{
   
virtual X* __foo() override;
   
virtual X& __bar() override;
    Y
* foo() { return static_cast<Y*>(this->foo()); }
    X
& bar() { return static_cast<Y&>(this->bar()); }
};

implicit conversion don't guarantee this round trip.  You will at least to show that there is a way to compile with the semantic you want.

I am not a compiler guy, but I thought it would be implemented more or less this way (borrowing the structure of the example you sketched, and assuming T is convertible to U):

struct A
{
   
void U foo() { return __foo(); }
   
virtual U __foo();
};

struct B : A
{
   
virtual U __foo() override { return U{foo()}; }
    T foo
();
};

Thank you for your comments.

Kind regards,

Andy

Andy Prowl

unread,
Dec 23, 2014, 10:22:09 AM12/23/14
to std-pr...@isocpp.org
On Tuesday, December 23, 2014 3:29:21 PM UTC+1, David Krauss wrote:

The idea has been floating around for a while. Are there open questions remaining? Has there been any formal write-up?

I did some quick research before posting here and did not find anything. I myself haven't written anything formal - I first wanted to get feedback from expert users.

Kind regards,

Andy

Farid Mehrabi

unread,
Dec 23, 2014, 10:28:30 AM12/23/14
to std-proposals
I think you got it upside down: 


struct A
{
    
X* foo() ;
    X& bar() ;
    X proposal() ;
//implement virtual:
    virtual X* __foo(){ return foo(); }
    virtual X& __bar(){ return bar(); }
    virtual X& __proposal(){ return proposal(); }
};

struct B : A
{
    Y* foo() ;
    Y
& bar() ;
    Y proposal() ;
//implement virtual:
    virtual X* __foo() override { return static_cast<X*>(this->foo()); };
    virtual X& __bar() override { return static_cast<X&>(this->bar()); };
    virtual X __proposal() override { return static_cast<X>(this->proposal()); };
};

regards,
FM.

Jean-Marc Bourguet

unread,
Dec 23, 2014, 11:28:02 AM12/23/14
to std-pr...@isocpp.org
That doesn't work if there is a C inheriting from B which override foo as well. This is fixable and as alluded by David, that could be a mean to implement the current proposal, at the cost of adding virtual functions at each level were covariant return is used.

struct R1 {};
struct R2: R1 {};
struct R3: R2 {};

struct C1 {
   virtual R1* foo();
};

struct C2: C1 {
   virtual R1* foo() { return static_cast<R1*>(foo__1()); }; // final descendant overrides instead foo__1
   virtual R2* foo__1();
};

struct C3: C2 {
   virtual R2* foo__1() { return static_cast<R2*>(foo__2()); }; // final, descendant overrides instead foo__2
   virtual R3* foo__2();
};


Yours

Ville Voutilainen

unread,
Dec 23, 2014, 12:13:05 PM12/23/14
to std-pr...@isocpp.org
Pointer conversions are fairly easy business. Converting from a value of
derived type to a value of base type isn't, nor is converting from unique_ptr<D>
to unique_ptr<B> or vector<D*> to vector<B*>. There may be ABI complications
involved, so this proposal should be looked at by an implementation vendor.

Andy Prowl

unread,
Dec 23, 2014, 12:22:32 PM12/23/14
to std-pr...@isocpp.org

Thank you for your feedback Ville. Just as a note, I am not proposing to allow converting a vector<D*> into a vector<B*>. Such a conversion currently does not exist, and it seems to me that whether it should exist or not is independent from the context of covariant return types.

The (user-defined) conversion from unique_ptr<D> to unique_ptr<B>, on the other hand, exists, and I wish covariant return types took user-defined conversions into account.

I am not sufficiently experienced to understand the implications in terms of ABI, and there is no formal proposal yet. Do you have suggestions on whom I can contact for feedback on feasibility? Or should I just write a proposal and let it be discussed?

Thank you in advance,

Andy

Thiago Macieira

unread,
Dec 23, 2014, 1:32:26 PM12/23/14
to std-pr...@isocpp.org
I think you based your request on the simplest example possible. Yes, it would
work for your example, but not for anything a little more complex.

struct X {};
struct Spacing { char buffer[100]; }
struct Y: Spacing, X {};

Now the covariant returns are permitted, but require a pointer-adjustment
thunk because the value of an X* differs from an Y* if they point to the same
object.

With that in mind, your request implies:
- alll smart pointers must be able to handle deleting of derived classes if
they delete, which isn't the case (example: std::unique_ptr with default
deleter)
- all smart pointers must be able to handle copying from a derived class
pointer or object containing the derived class
- the compiler must know how to copy

The first and last requirements are a non-starter.
--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Andy Prowl

unread,
Dec 23, 2014, 2:12:39 PM12/23/14
to std-pr...@isocpp.org
On Tuesday, December 23, 2014 7:32:26 PM UTC+1, Thiago Macieira wrote:

I think you based your request on the simplest example possible. Yes, it would
work for your example, but not for anything a little more complex.

struct X {};
struct Spacing { char buffer[100]; }
struct Y: Spacing, X {};

Now the covariant returns are permitted, but require a pointer-adjustment
thunk because the value of an X* differs from an Y* if they point to the same
object.

With that in mind, your request implies:
 - alll smart pointers must be able to handle deleting of derived classes if
   they delete, which isn't the case (example: std::unique_ptr with default
   deleter)
 - all smart pointers must be able to handle copying from a derived class
   pointer or object containing the derived class
 - the compiler must know how to copy

The first and last requirements are a non-starter.
--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
   Software Architect - Intel Open Source Technology Center
      PGP/GPG: 0x6EF45358; fingerprint:
      E067 918B B660 DBD1 105C  966C 33F5 F005 6EF4 5358

If I understand your first point correctly, then no, I don't think all smart pointers should be able to handle deleting of derived classes. I think it's a user's responsibility to make sure that the base class X has a virtual destructor if they plan to delete objects of type Y polymorphically (e.g. through a unique_ptr<X>). After all, even today, nothing prevents the user (with reference to your example) from deleting a pointer of type X* which is pointing to an object of type Y obtained by invoking an overriding virtual function - since Y* is covariant with X*, a function returning Y* can override a function returning X*. In my view, allowing a conversion from unique_ptr<Y> to unique_ptr<X> does not change anything here. That conversion exists and deletion of an object of type Y through a unique_ptr<X> can bite the user in several other contexts. I don't see why special concerns should be raised for this use case.

I am not sure I understand points 2. and 3. - in particular, I don't understand if they follow from point 1., and whether the answer above indirectly addresses those as well. What do you mean by "the compiler must know how to copy"? What is to be copied?

Kind regards,

Andy

Thiago Macieira

unread,
Dec 23, 2014, 2:22:22 PM12/23/14
to std-pr...@isocpp.org
On Tuesday 23 December 2014 11:12:38 Andy Prowl wrote:
> I am not sure I understand points 2. and 3. - in particular, I don't
> understand if they follow from point 1., and whether the answer above
> indirectly addresses those as well. What do you mean by "the compiler must
> know how to copy"? What is to be copied?

Suppose what you asked for were allowed:

struct X {};
struct Spacing { char buffer[100]; }
struct Y: Spacing, X {};

struct A
{
virtual std::shared_ptr<X> foo();
};

struct B : A
{
virtual std::shared_ptr<Y> foo() override;
};

void f(A *a)
{
a->foo();
}

The call to foo() must be placed virtually, since the compiler doesn't know
what the real tyoe of A is. If it's actually B, then that means it needs to
call, somehow, B::foo(), which returns std::shared_ptr<Y>, then somehow
copy/move that returned value into a std::shared_ptr<X>.

Of course that the compiler can't know how to do that. It needs to call a
helper function, probably the template move assignment operator or template
move constructor from std::shared_ptr.

If that is permitted, what's stopping one from writing:

struct A
{
virtual Foo foo();
};

struct B : A
{
virtual Bar foo() override;
};

for two unrelated types Foo and Bar, provided that

Bar b{Foo()};

compiles?

Andy Prowl

unread,
Dec 23, 2014, 2:31:59 PM12/23/14
to std-pr...@isocpp.org
On Tuesday, December 23, 2014 8:22:22 PM UTC+1, Thiago Macieira wrote:

If that is permitted, what's stopping one from writing:

struct A
{
    virtual Foo foo();
};

struct B : A
{
    virtual Bar foo() override;
};

for two unrelated types Foo and Bar, provided that

        Bar b{Foo()};

compiles?

Nothing. What I'm proposing is to take into consideration *any* viable implicit conversions, including user-defined ones; the conversions from unique_ptr<D> to unique_ptr<B>, from shared_ptr<D> to shared_ptr<B>, from D* to B*, from D& to B&, etc. would be just particular cases of the general rule.

I can't find a good reason why your example above should not compile. If "Bar b{Foo()};" (or rather, "Bar b = Foo();") compiles, this means a Foo can be provided wherever a Bar is expected: in that case, why should we prevent an overriding function to return a Foo when the caller of the overridden function is expecting a Bar?

Kind regards,

Andy

Thiago Macieira

unread,
Dec 23, 2014, 2:54:31 PM12/23/14
to std-pr...@isocpp.org
On Tuesday 23 December 2014 11:31:58 Andy Prowl wrote:
> Nothing. What I'm proposing is to take into consideration *any* viable
> implicit conversions, including user-defined ones; the conversions from
> unique_ptr<D> to unique_ptr<B>, from shared_ptr<D> to shared_ptr<B>, from
> D* to B*, from D& to B&, etc. would be just particular cases of the general
> rule.

Why do a covariant override at all?

In my experience, most covariant overrides can be replaced by a non-covariant
override returning the original type and adjust the caller code to up-cast as
needed.

> I can't find a good reason why your example above should not compile. If
> "Bar b{Foo()};" (or rather, "Bar b = Foo();") compiles, this means a Foo
> can be provided wherever a Bar is expected: in that case, why should we
> prevent an overriding function to return a Foo when the caller of the
> overridden function is expecting a Bar?

Data loss. Foo and Bar may be copyable, but may not be supersets of one
another.

Given that same rule, then:

struct A
{
virtual unsigned foo();
};

struct B : A
{
unsigned long long x;
virtual unsigned long long foo() override
{ return x; }
};

The above also compiles and you can virtually call foo() on A, getting a
truncated value without any warnings.

Andy Prowl

unread,
Dec 23, 2014, 3:24:39 PM12/23/14
to std-pr...@isocpp.org
On Tuesday, December 23, 2014 8:54:31 PM UTC+1, Thiago Macieira wrote:
On Tuesday 23 December 2014 11:31:58 Andy Prowl wrote:
> Nothing. What I'm proposing is to take into consideration *any* viable
> implicit conversions, including user-defined ones; the conversions from
> unique_ptr<D> to unique_ptr<B>, from shared_ptr<D> to shared_ptr<B>, from
> D* to B*, from D& to B&, etc. would be just particular cases of the general
> rule.

Why do a covariant override at all?

In my experience, most covariant overrides can be replaced by a non-covariant
override returning the original type and adjust the caller code to up-cast as
needed.

I'm not sure what you mean here, unless you meant to write "down-cast" instead of "up-cast". That's of course possible (and pretty much unavoidable as of today), but unnecessary IMO. The logic seems sound to me: if there is an implicit conversion from B to A, then this means I am allowed to provide a B whenever an A is expected. Since the caller of the base class's virtual function expects an A, it makes sense to allow the overriding virtual function to provide a B (which would get implicitly converted). 
 
> I can't find a good reason why your example above should not compile. If
> "Bar b{Foo()};" (or rather, "Bar b = Foo();") compiles, this means a Foo
> can be provided wherever a Bar is expected: in that case, why should we
> prevent an overriding function to return a Foo when the caller of the
> overridden function is expecting a Bar?

Data loss. Foo and Bar may be copyable, but may not be supersets of one
another. 

But then the problem is not specific to this particular use case in which that lossy conversion would be used. If the user does not want a lossy conversion from Foo to Bar (or vice versa), then it should simply not use it (or not define it) - either directly or in the context of covariant return types. If the user overrides a function returning a Foo by a function returning a Bar, and the conversion from Bar to Foo is lossy, I expect the user to know what they are doing.
 
Given that same rule, then:

struct A
{
    virtual unsigned foo();
};

struct B : A
{
    unsigned long long x;
    virtual unsigned long long foo() override
    { return x; }
};

The above also compiles and you can virtually call foo() on A, getting a
truncated value without any warnings.

Why "without any warnings"? When you compile B, the compiler has all the necessary information for emitting a warning. Again, I don't see how this differs from:

unsigned long long x = ...;
unsigned y = x;

If I am writing the above, it means I know what I am doing. If the data loss matters, then I'm doing the wrong thing, yet nobody is stopping me. Why should we try to stop the user from doing the same in the context of covariant return types?

Kind regards,

Andy
 

Matthew Woehlke

unread,
Dec 23, 2014, 3:58:51 PM12/23/14
to std-pr...@isocpp.org
On 2014-12-23 14:12, Andy Prowl wrote:
> On Tuesday, December 23, 2014 7:32:26 PM UTC+1, Thiago Macieira wrote:
>> - alll smart pointers must be able to handle deleting of derived classes
>> if they delete, which isn't the case (example: std::unique_ptr with
>> default deleter)
>
> If I understand your first point correctly, then no, I don't think all
> smart pointers should be able to handle deleting of derived classes. I
> think it's a user's responsibility to make sure that the base class X has a
> virtual destructor if they plan to delete objects of type Y polymorphically
> (e.g. through a unique_ptr<X>). After all, even today, nothing prevents the
> user (with reference to your example) from deleting a pointer of type X*
> which is pointing to an object of type Y obtained by invoking an overriding
> virtual function - since Y* is covariant with X*, a function returning Y*
> can override a function returning X*.

Of course, the compiler may be able to (and if it can, *ought* to)
diagnose this. I.e. if a covariant return converts from
unique_ptr<Derived> to unique_ptr<Base>, and Base does *not* have a
virtual dtor, that is an error. (In fact, such unique_ptr conversion
should very possible be an error, period. A static_assert in the
conversion operator could probably be used for this...)

--
Matthew

Matthew Woehlke

unread,
Dec 23, 2014, 4:21:32 PM12/23/14
to std-pr...@isocpp.org
On 2014-12-23 14:22, Thiago Macieira wrote:
> [...]
> Of course that the compiler can't know how to do that. It needs to call a
> helper function, probably the template move assignment operator or template
> move constructor from std::shared_ptr.
>
> If that is permitted, what's stopping one from writing:
>
> struct A
> {
> virtual Foo foo();
> };
>
> struct B : A
> {
> virtual Bar foo() override;
> };
>
> for two unrelated types Foo and Bar, provided that
>
> Bar b{Foo()};
>
> compiles?

Why does it need to be prevented?

Example internal implementation:

struct A
{
virtual Foo foo() { return A::__foo(); }
private: Foo __foo();
};

struct B
{
virtual Bar foo() { return B::__foo(); } // shadows A::foo!

private:
Bar __foo();
virtual Foo foo() override final
{ return Foo{foo()}; } // calls B's virtual 'Bar foo()'
};

IOW, the virtual dispatch calls a flavor of foo() based on the type of
the pointer via which the method is invoked. Covariant return creates a
hidden thunk which converts from B's return type to A's return type
which supplies A's vtable entry. B makes that override final and hidden,
supplying a *NEW* vtable entry which returns B's return type and can be
overridden in derived classes as usual.

It's not much different from overloading (which can also hide base
virtual members), except the overload is based on the return type
instead of the parameters, with the addition that the hidden virtual
method of the base class has an implicit converting implementation.

On 2014-12-23 14:54, Thiago Macieira wrote:
> Why do a covariant override at all?
>
> In my experience, most covariant overrides can be replaced by a non-covariant
> override returning the original type and adjust the caller code to up-cast as
> needed.

It seems to me that the point would be to avoid this... (Anyway, in case
of return by value, allowing the above means less work or may avoid data
loss.)

--
Matthew

Matthew Woehlke

unread,
Dec 23, 2014, 4:28:52 PM12/23/14
to std-pr...@isocpp.org
Well, -Wconversion is not on by default. I don't think the above warns
by default, either, but I agree both should emit the same warning. (And
also I agree that they aren't conceptually different.)

Although, Thiago's example may or may not be legal depending on whether
the conversion is defined as construction (i.e. brace initialization) or
a static_cast. The former is actually ill-formed for the above and
should error in this case; i.e. covariant returns would require a user
defined conversion operator or constructor.

--
Matthew

Thiago Macieira

unread,
Dec 23, 2014, 4:35:52 PM12/23/14
to std-pr...@isocpp.org
On Tuesday 23 December 2014 12:24:39 Andy Prowl wrote:
> > Given that same rule, then:
> >
> > struct A
> > {
> >
> > virtual unsigned foo();
> >
> > };
> >
> > struct B : A
> > {
> >
> > unsigned long long x;
> > virtual unsigned long long foo() override
> > { return x; }
> >
> > };
> >
> > The above also compiles and you can virtually call foo() on A, getting a
> > truncated value without any warnings.
>
> Why "without any warnings"? When you compile B, the compiler has all the
> necessary information for emitting a warning. Again, I don't see how this
> differs from:

Please tell me where the compiler would print a warning here:

struct A
{
virtual unsigned foo();
}

void f(A *a)
{
a->foo();
}

Because that's all it sees at the call site.

Or are you saying that it should generate a warning on the following, without
the definition of the function?

struct B: A
{
virtual unsigned long long foo() override;
};

"warning: conversion from `unsigned long long' to `unsigned' may alter its
value in the return of virtual override of foo()"

> unsigned long long x = ...;
> unsigned y = x;
>
> If I am writing the above, it means I know what I am doing. If the data
> loss matters, then I'm doing the wrong thing, yet nobody is stopping me.
> Why should we try to stop the user from doing the same in the context of
> covariant return types?

Because in the above, there was an explicit assignment of a larger integer to
a smaller one, even if the cast itself was implicit.

unsigned f(unsigned long long y) { return y; }
=> warning: conversion to ‘unsigned int’ from ‘long long unsigned int’ may
alter its value [-Wconversion]

But I can silence such a warning with:
unsigned f(unsigned long long y) { return unsigned(y); }'

How do you suggest modifying the definition of B above so the compiler knows
not to emit a warning?

Andy Prowl

unread,
Dec 23, 2014, 4:38:10 PM12/23/14
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Tuesday, December 23, 2014 10:28:52 PM UTC+1, Matthew Woehlke wrote:
 
Although, Thiago's example may or may not be legal depending on whether
the conversion is defined as construction (i.e. brace initialization) or
a static_cast. The former is actually ill-formed for the above and
should error in this case; i.e. covariant returns would require a user
defined conversion operator or constructor.

Assuming that the overridden virtual function has return type T, I think an overriding function can have return type U if and only if some invented variable of type T can be copy-initialized from an rvalue of type U.

Thiago Macieira

unread,
Dec 23, 2014, 4:40:51 PM12/23/14
to std-pr...@isocpp.org
On Tuesday 23 December 2014 15:58:32 Matthew Woehlke wrote:
> Of course, the compiler may be able to (and if it can, *ought* to)
> diagnose this. I.e. if a covariant return converts from
> unique_ptr<Derived> to unique_ptr<Base>, and Base does *not* have a
> virtual dtor, that is an error. (In fact, such unique_ptr conversion
> should very possible be an error, period. A static_assert in the
> conversion operator could probably be used for this...)

Unfortunately, this currently compiles:

struct X {};
struct Y : X { ~Y(); };

std::unique_ptr<Y> f();
void g()
{
std::unique_ptr<X> x = f();
}

There are no warnings and the Y destructor is not called.

Whether there is a valid use-case for erroring out on this or not, I can't
say.

Andy Prowl

unread,
Dec 23, 2014, 4:59:39 PM12/23/14
to std-pr...@isocpp.org
On Tuesday, December 23, 2014 10:35:52 PM UTC+1, Thiago Macieira wrote: 

Please tell me where the compiler would print a warning here:

struct A
{
        virtual unsigned foo();
}

void f(A *a)
{
        a->foo();
}

Because that's all it sees at the call site.

Correct, the compiler cannot generate any warning here, but I do not see this as a problem. The client is only concerned with the contract of A::foo(), and shall assume that any subtype of A is able to fulfill A::foo()'s contract. Now if a subtype of A called B is defined in a way that it fulfills A::foo()'s contract (i.e. returning an unsigned long) by truncating a value of type unsigned long long, then this is a something the implementer of B has to be warned about, not the client of A. The compiler should be able to warn during the compilation of B.
 
Or are you saying that it should generate a warning on the following, without
the definition of the function?

struct B: A
{
        virtual unsigned long long foo() override;
};

"warning: conversion from `unsigned long long' to `unsigned' may alter its
value in the return of virtual override of foo()"

Yes, this is what I meant.
 
> unsigned long long x = ...;
> unsigned y = x;
>
> If I am writing the above, it means I know what I am doing. If the data
> loss matters, then I'm doing the wrong thing, yet nobody is stopping me.
> Why should we try to stop the user from doing the same in the context of
> covariant return types?

Because in the above, there was an explicit assignment of a larger integer to
a smaller one, even if the cast itself was implicit.

unsigned f(unsigned long long y) { return y; }
 => warning: conversion to ‘unsigned int’ from ‘long long unsigned int’ may
alter its value [-Wconversion]

But I can silence such a warning with:
unsigned f(unsigned long long y) { return unsigned(y); }'

How do you suggest modifying the definition of B above so the compiler knows
not to emit a warning?

Good question. Compiler-specific directives are one possibility, although I do understand it is not a very elegant solution.

Kind regards,

Andy

Matthew Woehlke

unread,
Dec 23, 2014, 5:40:07 PM12/23/14
to std-pr...@isocpp.org
...or if there exists a conversion operator, e.g. 'U::operator T'. Or,
put differently, if the following is legal:

U&& u = ...;
return T{u};

But basically, yes, that's what I was trying to say. I think we're on
the same page.

--
Matthew

Matthew Woehlke

unread,
Dec 23, 2014, 5:45:38 PM12/23/14
to std-pr...@isocpp.org
On 2014-12-23 16:35, Thiago Macieira wrote:
> Please tell me where the compiler would print a warning [...]
> Or are you saying that it should generate a warning on the following, without
> the definition of the function?
>
> struct B: A
> {
> virtual unsigned long long foo() override;
> };
>
> "warning: conversion from `unsigned long long' to `unsigned' may alter its
> value in the return of virtual override of foo()"

Right. In order to satisfy A's vtable, there exists an implementation in
B of 'virtual unsigned foo() override /*final*/'. Wherever that is
compiled, I would expect a warning. (I guess that would be in any TU
that includes the definition of B, since the definition is effectively
inline.)

If we go with the 'as if brace initialized', you'd actually get a
-Wnarrowing warning (or error).

> On Tuesday 23 December 2014 12:24:39 Andy Prowl wrote:
>> unsigned long long x = ...;
>> unsigned y = x;
>>
>> If I am writing the above, it means I know what I am doing. If the data
>> loss matters, then I'm doing the wrong thing, yet nobody is stopping me.
>> Why should we try to stop the user from doing the same in the context of
>> covariant return types?
>
> Because in the above, there was an explicit assignment of a larger integer to
> a smaller one, even if the cast itself was implicit.

...and you still get a -Wconversion warning, which we expect as a
minimum for the covariant return case also (if not -Wnarrowing).

> But I can silence such a warning with:
> unsigned f(unsigned long long y) { return unsigned(y); }'
>
> How do you suggest modifying the definition of B above so the compiler knows
> not to emit a warning?

Either use a pragma, or don't use a covariant return that results in a
warning in the first place :-).

In practice I would expect covariant returns of integral types to be
much less common than of [pointers to] class types.

If we *really* need this, I almost feel like the correct solution is to
allow the user to supply definitions of both the base override and the
flavor with the covariant return type. (Which, yeah, violates the 'no
overload on return type' rule... we'd need a careful exception.)

--
Matthew

Andy Prowl

unread,
Dec 23, 2014, 5:52:00 PM12/23/14
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Tuesday, December 23, 2014 11:45:38 PM UTC+1, Matthew Woehlke wrote:

Either use a pragma, or don't use a covariant return that results in a
warning in the first place :-).

In practice I would expect covariant returns of integral types to be
much less common than of [pointers to] class types.
 
I share your thoughts on this.
 
If we *really* need this, I almost feel like the correct solution is to
allow the user to supply definitions of both the base override and the
flavor with the covariant return type. (Which, yeah, violates the 'no
overload on return type' rule... we'd need a careful exception.)

Personally, I would not go that far. If the only purpose of this exception would be to avoid using pragmas for silecing warnings about narrowing conversions for covariant return types, it is not worth it. That's my impression at least.

Kind regards,

Andy

Matthew Woehlke

unread,
Dec 23, 2014, 5:52:27 PM12/23/14
to std-pr...@isocpp.org
On 2014-12-23 16:40, Thiago Macieira wrote:
> On Tuesday 23 December 2014 15:58:32 Matthew Woehlke wrote:
>> Of course, the compiler may be able to (and if it can, *ought* to)
>> diagnose this. I.e. if a covariant return converts from
>> unique_ptr<Derived> to unique_ptr<Base>, and Base does *not* have a
>> virtual dtor, that is an error. (In fact, such unique_ptr conversion
>> should very possible be an error, period. A static_assert in the
>> conversion operator could probably be used for this...)
>
> Unfortunately, this currently compiles:
>
> struct X {};
> struct Y : X { ~Y(); };
>
> std::unique_ptr<Y> f();
> void g()
> {
> std::unique_ptr<X> x = f();
> }
>
> There are no warnings and the Y destructor is not called.

"Unfortunate" is indeed the correct word :-).

As far as the original topic, I don't think we should make the behavior
here different for covariant return types. If you can write such broken
code without covariant returns involved, having it suddenly diagnose
because it's happening via a covariant return seems a little odd. If we
want to diagnose it, we should diagnose it here also.

(I don't think we had the necessary introspection to be *able* to
diagnose this as of C++11, which probably has something to do with why
it isn't being diagnosed... At the very least, this should trip some
warning, e.g. -Wnon-virtual-dtor.)

--
Matthew

Matthew Woehlke

unread,
Dec 23, 2014, 5:59:34 PM12/23/14
to std-pr...@isocpp.org
On 2014-12-23 17:52, Matthew Woehlke wrote:
> On 2014-12-23 16:40, Thiago Macieira wrote:
>> Unfortunately, this currently compiles:
>>
>> struct X {};
>> struct Y : X { ~Y(); };
>>
>> std::unique_ptr<Y> f();
>> void g()
>> {
>> std::unique_ptr<X> x = f();
>> }
>>
>> There are no warnings and the Y destructor is not called.
>
> "Unfortunate" is indeed the correct word :-).
>
> (I don't think we had the necessary introspection to be *able* to
> diagnose this as of C++11, which probably has something to do with why
> it isn't being diagnosed... At the very least, this should trip some
> warning, e.g. -Wnon-virtual-dtor.)

Meanwhile I've filed https://gcc.gnu.org/bugzilla/show_bug.cgi?id=64399

--
Matthew

Andy Prowl

unread,
Dec 23, 2014, 6:00:04 PM12/23/14
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Tuesday, December 23, 2014 11:52:27 PM UTC+1, Matthew Woehlke wrote:
 
(I don't think we had the necessary introspection to be *able* to
diagnose this as of C++11, which probably has something to do with why
it isn't being diagnosed... At the very least, this should trip some
warning, e.g. -Wnon-virtual-dtor.)
 
Actually, C++11 has the std::has_virtual_destructor type trait, so unless I'm missing something the risky conversions for unique_ptr could have been ruled out by design library-wise (shared_ptr has a type-erased deleter, so this discussion does not apply to it). Perhaps allowing those conversions was a conscious decision?

Kind regards,

Andy

Matthew Woehlke

unread,
Dec 23, 2014, 6:05:06 PM12/23/14
to std-pr...@isocpp.org
Agreed. I would hope that in actual practice this is not an issue and
can be ignored (or at least, is not sufficiently an issue that folks
would take umbrage to using pragmas or the like to work around it).
That's why I emphasized "really" in the above :-).

--
Matthew

Thiago Macieira

unread,
Dec 23, 2014, 8:37:30 PM12/23/14
to std-pr...@isocpp.org
You can delete it properly by casting it to the right type before delete or by
moving it to a right-type std::unique_ptr.
Reply all
Reply to author
Forward
0 new messages