overriding virtual function return type differs and is not covariant
Asked Answered
H

6

9

Ah, SO came back just in time.

I am getting a strange error:

 'B::blah': overriding virtual function return type differs and is not covariant from 'A::blah'

Here is the code causing the problem:

class A {
public:
    class Inner { };

    virtual Inner blah() = 0;
};

class B : public A {
public:
    class Inner2 : public Inner { };

    Inner2 blah() {
        return Inner2();
    }
};

I looked up the error, and according to a page I found on the Microsoft website, one of the ways types can be covariant is if:

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

Is that not the case with Inner and Inner2? I am using Microsoft Visual C++ 2010 if it matters.


OK, so thanks to John I learned that only pointers and references can be covariant. Why is that? A Derived can be cast to a Base, so why don't virtual functions with return types that are derived from the same thing just cast the return type to the one of the base class? In my example, it seems like it would make sense to have (A*(new B))->blah() return a Inner that is really an Inner2 that has been cast up.

Hypostasize answered 6/8, 2011 at 21:27 Comment(0)
B
18

Only pointers and references can be covariant.

Bissau answered 6/8, 2011 at 21:29 Comment(18)
John, will you read my comment on Sorens answer and see if you can answer that as well?Hypostasize
The reason you can't covariantly return value types is that those types can have different size. For example A::Inner may be 4 bytes long and B::Inner2 8 bytes long. And yes, you can't return reference to local object. But this is somehow different problem than asked. Bottom line: you will need to return pointer and somehow manage its lifetime.Bissau
you can return a derived class from a normal function whose signature says it returns a Base, so why can't you do that for virtual functions? Like, Base test() { return Derived(); } works.Hypostasize
@Seth: That's called slicing, because Base has a copy constructor Base(const Base &) which (unfortunately) has to accept a const Derived &, but this is almost guaranteed not to be what you want.Reeba
In case of Base test() { return Derived(); }, first you create Derived then create Base using copy constructor. If you won't provide user constructor it will just discard extra Derived members. You can't change return type for value object, because if caller is expecting 4-bytes long Base then receiving 8-byte long Derived would make serious mess around stack.Bissau
@Kerrek I know of slicing, and in this case it is exactly what I want. Slicing is better than completely keeping me from doing something. And still, that doesn't answer why it doesn't work.Hypostasize
If you're fine with slicing then just return A::Inner in derived class.Bissau
@Bissau no, I need callers who know that a particular pointer is a Derived* to get a Derived* back, and classes who only know it's a Base* to get a Base* back.Hypostasize
@Seth: the question is where the slicing is done and the calling conventions. Consider that if you call through a reference to base then the caller will allocate space for the function to return a inner object, and there will be no more space than that. If the final overrider were to write a inner2 it would write beyond the space allocated, and that would be undefined behavior. Note that this is different with John's last comment, where the slicing is performed inside the virtual function, and only the inner subobject will be written.Fanchan
@David that makes sense, but like you said, why isn't it made so that the function does the slicing? You'd think C++ could know what type was expected to be returned and which one was actually being returned.Hypostasize
@Seth: The compiler, when processing the function does not know who is going to make the call, it does not know whether the static type at the place of call is base or derived, and it has to provide a single definition. If you want the function to do the slicing, then return a inner object, but that will not let you return different things depending on the static type at the place of call. Alternatively you can provide two functions, a non-virtual that returns inner2 and the virtual that returns inner (forwards the call to the new function, slices and returns it)Fanchan
@David thanks, that answers my question. And yeah, I had gone with making two functions, one virtual and one not, I just wish it were possible to do it the other way.Hypostasize
@Seth: You should revisit your design. Depending on slicing is a strong code smell, and it might just be that you are taking the wrong approach to solve the actual problem. If you want polymorphic behavior you should do with references and pointers, not values. I don't think the language should allow that, as I have not ever seen a good use case for the feature, and the compiler will catch errors from people not understanding the difference between value and reference semantics. Slicing is confusing enough as it is to allow more valid cases in the language.Fanchan
@DavidRodríguez-dribeas "the question is where the slicing is done" Somewhere. Not in the calling code obviously. "and the calling conventions." Which convention? The one for Base or for Derived?Beer
"The reason you can't covariantly return value types is that those types can have different size." Wrong. There is no reason. It's like that, period.Beer
@curiousguy: If you want to believe there is no reason feel free. On the other hand, you can try to think on the calling conventions (take the Itanium C++ ABI as a good example) and try to think how to to implement dynamic dispatch allowing for covariant return of value types. Of course, you could design a completely different object model (the callee allocates the space in the stack and returns the object and size that was allocated), but that would not be the C++ object model (or philosophy, as there would be added costs for each call). Most design decisions have a justification.Fanchan
@curiousguy: Regarding the previous comment (the question is where the slicing is done) I am not sure I made myself clear. If there is no covariant value return type, the function would slice on the return expression. In a previous comment Seth suggested that he'd rather have the object sliced (if the callee does not slice, the alternative is the slicing to happen in the caller), and there it cannot be safely done, as the reserved space is what it is (and it is reserved by the caller, that is the calling convention and it is not Base/Derived, it is platform/compiler).Fanchan
@curiousguy: On a personal matter, I find that you tend to assert rather than reason. I think it would benefit everyone if rather than claiming something to be a fact (There is no reason. It's like that, period) you provided alternatives. I understand that if there is no reason for the design to be like it is, there will be viable alternatives. With that on the table it would be much easier to determine whether those alternatives are valid and thus there really is no reason, or whether the alternatives have worse flaws.Fanchan
N
3

Imagine that your example worked.

Consider the following code:

A *a = some_function();
A::Inner inner = a->blah();

If the dynamic type of a is B*, then a->blah() calls a::B->blah(), and returns a B::Inner. This is then silently sliced into an instance of A::Inner. In general, this (and any type of slicing) is not what you want. I find this to be a good restriction.

Nonesuch answered 28/5, 2013 at 10:0 Comment(2)
In general, this (and any type of slicing) is allowed in C++.Beer
@Beer Indeed it is, and it's nearly always a bug.Nonesuch
V
2

If what you request would be allowed it would mean that a call to blah() via the base class must do a conversion from Inner2 to Inner... a cast is not possible since the caller is responsible for managing the returned object (since it is not returned by pointer/reference but by value) and will reserve space for it on the stack. So it can only handle Inner not Inner2 or whatever ancestor class.

So you then have an instance of Inner, not Inner2... So you don't get any advantage...

Veiled answered 6/8, 2011 at 22:19 Comment(0)
B
0

Why is that?

Simply because the ISO C++ standard committee ruled it that way!

There is no fundamental reason. It could be done differently, at the cost of a tiny additional compiler complexity.

Beer answered 27/5, 2013 at 18:47 Comment(3)
It's not considered "tiny" in the domains where C++'s efficiency matters, and is fundamentally incompatible with how "by value" returns work.Tanaka
@JimmyHartzell Efficiency of what?Beer
@JimmyHartzell How is that "fundamentally incompatible with how "by value" returns work"?Beer
C
0

objects declared on the runtime stack must be known to the compiler at compile time:

void f(const A & a) {
   Inner inr = a.blah( );

}

inr must be of both static and dynamic type "Inner" (not "Inner2") to be allocated off the stack -- so if blah returned an Inner2 it would be used to construct an Inner and it's "2ness" would be lost (or sliced as K Dragon mentions).

Cadman answered 29/7, 2013 at 19:0 Comment(2)
"it's "2ness" would be lost" which seems to be the intent of the code writer here. When you declare an Inner instance, you get an Inner instance, not an Inner2 instance. What is the problem here?Beer
The code writer states in the question "return a Inner that is really an Inner2 that has been cast up."Cadman
S
-1

The blah method have to return A::Inner in all cases -- if you think of the reason it is quite simple. A instance of B could easily be case to the base class A. Now if anybody calls blah on A (the base class which object should it return? Inner2 or Inner?

Sid answered 6/8, 2011 at 21:39 Comment(4)
Like you said, a B can be cast to an A, but likewise an Inner2 can be cast to an Inner. So when blah is called on a B which is cast to an A, it would return an Inner2 cast to an Inner. Why doesn't it do that?Hypostasize
The return types cannot be different between the two -- you can simpy just make B instantiate a Inner2, but return it as a ref to Inner, and that would solve it as well -- I will update the answer to refects this.Sid
no, I can't do that because that would be returning a reference to a local object.Hypostasize
-1. As soon as you use the result of B::blah you enter undefined behavior land.Prehuman

© 2022 - 2024 — McMap. All rights reserved.