Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can C++ ternary operator return type not be a base class type? [duplicate]

Consider this piece of code:

struct Base
{
    int x;
};

struct Bar : Base
{
    int y;
};

struct Foo : Base
{
    int z;
};

Bar* bar = new Bar;
Foo* foo = new Foo;

Base* returnBase()
{
    Base* obj = !bar ? foo : bar;
    return obj;
}

int main() {
    returnBase();
    return 0;
}

This doesn't work under Clang or GCC, giving me :

error: conditional expression between distinct pointer types ‘Foo*’ and ‘Bar*’ lacks a cast Base* obj = !bar ? foo : bar;

Which means for it to compile I have to change the code to :

Base* obj = !bar ? static_cast<Base*>(foo) : bar;

Since an implicit cast to a Base* exists, what is preventing the compiler from doing so?

In other words, why does Base* obj = foo; work without a cast but using the ?: operator doesn't? Is it because it's not clear that I want to use the Base part?

like image 988
Hatted Rooster Avatar asked Nov 15 '22 08:11

Hatted Rooster


2 Answers

Quoting from C++ standard draft N4296, Section 5.16 Conditional operator, Paragraph 6.3:

  • One or both of the second and third operands have pointer type; pointer conversions (4.10) and qualification conversions (4.4) are performed to bring them to their composite pointer type (Clause 5). The result is of the composite pointer type.

Section 5 Expressions, Paragraph 13.8 and 13.9:

The composite pointer type of two operands p1 and p2 having types T1 and T2, respectively, where at least one is a pointer or pointer to member type or std::nullptr_t, is:

  • if T1 and T2 are similar types (4.4), the cv-combined type of T1 and T2;
  • otherwise, a program that necessitates the determination of a composite pointer type is ill-formed.

Note: I copied 5/13.8 here just to show you that it doesn't hit. What's actually in effect is 5/13.9, "the program is ill-formed".

And Section 4.10 Pointer conversions, Paragraph 3:

A prvalue of type “pointer to cv D”, where D is a class type, can be converted to a prvalue of type “pointer to cv B”, where B is a base class (Clause 10) of D. If B is an inaccessible (Clause 11) or ambiguous (10.2) base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion is a pointer to the base class subobject of the derived class object. The null pointer value is converted to the null pointer value of the destination type.

So, it doesn't matter (at all) that both Foo and Bar are derived from one same base class. It only matters that a pointer to Foo and a pointer to Bar are not convertible to each other (no inheritance relationship).

like image 175
iBug Avatar answered Dec 14 '22 22:12

iBug


Allowing a conversion to the base pointer type for the conditional operator sounds nice but would problematic in practice.

In your example

struct Base {};
struct Foo : Base {};
struct Bar : Base {};

It might seem like the obvious choice for the type of cond ? foo : bar to be Base*.

But that logic doesn't hold up for a general case

E.g.:

struct TheRealBase {};
struct Base : TheRealBase {};
struct Foo : Base {};
struct Bar : Base {};

Should cond ? foo : bar be of type Base* or of type TheRealBase*?

How about:

struct Base1 {};
struct Base2 {};
struct Foo : Base1, Base2 {};
struct Bar : Base1, Base2 {};

What type should cond ? foo : bar be now?

Or how about now:

struct Base {};

struct B1 : Base {};
struct B2 : Base {};

struct X {};

struct Foo : B1, X {};
struct Bar : B2, X {};


      Base
      /  \
     /    \   
    /      \
  B1        B2 
   |   X    |
   | /   \  |
   |/     \ |
  Foo      Bar

     
      

Ouch!! Good luck reasoning for a type of cond ? foo : bar. I know, ugly ugly, non-practical and being hunted worthy, but the standard would still have to have rules for this.

You get the point.

And also keep in mind that std::common_type is defined in terms of the conditional operator rules.


Hm... These are excellent examples of situations that couldn't work, because of ambiguity. But C++ has many conversion rules that do work as long as the conversion is unambiguous. Like in this question. No?

Allowing just the unambiguous case here would be extremely problematic. The simple act of adding a base class could make the program uncompilable:

Initial codebase:

struct Base {};
struct Foo : Base {};
struct Bar : Base {};

This would allow __ ? foo : bar; to be written. And now you can't modify the inheritance structure because almost any modification would break existing code that uses the ternary operator in a legitimate way:

struct FooBarCommon {};

struct Base {};
struct Foo : Base, FooBarCommon {};
struct Bar : Base, FooBarCommon {};
struct Baz : Base {};

This seems a reasonable modifications. As the rules are now you can do this as long as you don't modify the public API of your classes. This won't be true anymore the standard would allow the conversions to base class only in unambiguous cases.

like image 28
bolov Avatar answered Dec 15 '22 00:12

bolov