Ternary operator implicit cast to base class
Asked Answered
R

5

57

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?

Robertoroberts answered 12/3, 2018 at 16:22 Comment(18)
Time to get the lawyers in. This gets my vote as the question of the day.Wareroom
fwiw: en.cppreference.com/w/cpp/language/…Coffeehouse
@IInspectable I'd rather leave it on classes instead of structs, there's a big chance that both have the same behavior but rather be safe.Robertoroberts
@user463035818 6.3) is the applicable rule, I believe? The rules that precede 6.3 all seem to fail to apply.Epigrammatist
The code was equivalent, just with a lot of noise removed. The change didn't affect the error either.Cecilacecile
@Cecilacecile Alright I'll take your word for it thenRobertoroberts
Did you try with this? Base* obj = !foo ? bar : foo; If you get same error for bar this time that means there is problem in compiler's logic. In such a condition you have to raise the question to vendor who has supplied you the compiler. I do not see any technical issue here.Foxhound
"Since an implicit cast to a Base* exists, what is preventing the compiler from doing so?" - why should try it? The compiler tries and conversion from Foo * to Bar * and fali; tries a conversion from Bar * to Foo * and fail. Why should try a conversion of both to Base *? And if there are more common bases, which pointer choose?Dorolisa
@AbhijitPritam I have, same error. I think it's unlikely that 3 major compilers all have a bug in them so I think this is standard somehow.Robertoroberts
@DrewDormann i didnt read it carefully. From what I saw I would expect that derived to base conversions are allowedCoffeehouse
@SombreroChicken, Did you check with if condition though?Foxhound
Clang 7.0.0 produces a slightly different error message: "error: incompatible operand types ('Foo *' and 'Bar *')" That looks to be more to the point, actually.Cecilacecile
@NathanOliver: If anything that should be closed to this as the duplicate target, as this one is posed in more general terms.Wareroom
@NathanOliver: Actually we need to hold our horses here. This is about pointer types in the ternary conditional, not class types.Wareroom
@Wareroom Nevermind. It is a different sittuationFrangipane
Implicit conversion to void* exists for both types, but we don't expect this conversion to actually fire, now do we?Producer
There is no such thing as an implicit cast in c++. The very concept of a cast means it is an explicit operation. What you mean it's an implicit conversionBonney
Well if you know what I meant then why tell meRobertoroberts
R
27

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).

Romine answered 12/3, 2018 at 16:36 Comment(1)
The issue with the code is that you can't determine the composite pointer type because you fall into the final bullet which says that a program that necessitates such a determination is ill-formed, as none of the other bullets are met. The closest bullet is arguably the "reference-related" one, since that's the bullet handling derived/base pointers. "Similar" basically means "same type ignoring cv-qualification at every level", which is plainly not met because these are pointers to different types. Pointer conversion rules are irrelevant; you can't even determine the type to convert them to.Teetotal
S
28

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.

Skiff answered 12/3, 2018 at 16:42 Comment(2)
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?Epigrammatist
I don't see how there's any problem in practice. You've outlined specific cases where the compiler couldn't easily determine what the return type of the ternary operator would be, but these aren't all cases. We already have rules for determining the return type of a ternary operator that exclude the question's example and the examples you provide. Why couldn't we just expand the rules to include the case in the question's example and exclude the cases in your examples? It's easy to tell when there is ambiguity, so why not only limit the rules to cases that are unambiguous?Stoma
R
27

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).

Romine answered 12/3, 2018 at 16:36 Comment(1)
The issue with the code is that you can't determine the composite pointer type because you fall into the final bullet which says that a program that necessitates such a determination is ill-formed, as none of the other bullets are met. The closest bullet is arguably the "reference-related" one, since that's the bullet handling derived/base pointers. "Similar" basically means "same type ignoring cv-qualification at every level", which is plainly not met because these are pointers to different types. Pointer conversion rules are irrelevant; you can't even determine the type to convert them to.Teetotal
L
6

In other words, why does Base* obj = foo; work without a cast but using the ?: operator doesn't?

The type of the conditional expression does not depend on the what it is assigned to. In your case, the compiler needs to be able to evaluate !bar ? foo : bar; regardless of what it is assigned to.

In your case, that is a problem since neither foo converted to type of bar nor bar can be converted to type of foo.

Is it because it's not clear that I want to use the Base part?

Precisely.

Lindner answered 12/3, 2018 at 16:35 Comment(5)
"The type of an expression does not depend on the what it is assigned to." That's not true in general. An expression naming an overloaded set of functions cannot be per se typed (e.g. decltype fails), but attains a proper type in a corresponding context. Perhaps adjust that to "type of a conditional..".Selectee
@Columbo, can you please point me to some place where I can get a deeper understanding of the subject?Lindner
This is basic stuff; overload a name f and write F* ptr = f;. I don't see what there is to clarify further.Selectee
Sorry if it came about abrasively, it just didn't know what to point to--clause 5 in the standard? :sSelectee
@Columbo, that's ok. Despite years of using C++, there are still a few aspects of the language that I am not aware of and some aspects of the language that don't jump out at me.Lindner
P
4

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

According to [expr.cond]/7,

Lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions are performed on the second and third operands. After those conversions, one of the following shall hold:

  • ...
  • One or both of the second and third operands have pointer type; pointer conversions, function pointer conversions, and qualification conversions are performed to bring them to their composite pointer type. The result is of the composite pointer type.

where composite pointer type is defined in [expr.type]/4:

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 both p1 and p2 are null pointer constants, std​::​nullptr_­t;

  • if either p1 or p2 is a null pointer constant, T2 or T1, respectively;

  • if T1 or T2 is “pointer to cv1 void” and the other type is “pointer to cv2 T”, where T is an object type or void, “pointer to cv12 void”, where cv12 is the union of cv1 and cv2;

  • if T1 or T2 is “pointer to noexcept function” and the other type is “pointer to function”, where the function types are otherwise the same, “pointer to function”;

  • if T1 is “pointer to cv1 C1” and T2 is “pointer to cv2 C2”, where C1 is reference-related to C2 or C2 is reference-related to C1, the cv-combined type of T1 and T2 or the cv-combined type of T2 and T1, respectively;

  • if T1 is “pointer to member of C1 of type cv1 U1” and T2 is “pointer to member of C2 of type cv2 U2” where C1 is reference-related to C2 or C2 is reference-related to C1, the cv-combined type of T2 and T1 or the cv-combined type of T1 and T2, respectively;

  • if T1 and T2 are similar types, the cv-combined type of T1 and T2;

  • otherwise, a program that necessitates the determination of a composite pointer type is ill-formed.

Now you can see a pointer to "common base" is not the composite pointer type.


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?

The problem is that the rule should detect the type of the conditional expression independently, without observing the initialization.

To be specific, a rule should detect the conditional expressions in the following two statements to be the same type.

Base* obj = !bar ? foo : bar;
bar ? foo : bar;

Now, if you have no doubt that the conditional expression in the second statement is ill-formed1, what's the reasoning to make it well-formed in the first statement?


1 Of course one can make a rule to make such expression well-formed. For example, let composite pointer types include the pointer to an unambiguous base type. However, this is something beyond this question, and should be discussed by ISO C++ committee.

Petiole answered 12/3, 2018 at 17:13 Comment(0)
A
0
exp1 ? exp2 : exp3

In the conditional operator, exp2 & exp3 must be of same type or alteast it should have type conversion function which converts one type to another. If not, then it is ill-formed.

    int i = 1;
    long j = 2;
    true  ? i : j; // OK
    false ? i : j; // OK


    string str1 = "hello";
    const char* str2 = "world";
    true  ? str1 : str2; // OK
    false ? str1 : str2; // OK

    int i = 1;
    string str1 = "hello";
    true  ? i : str1; // Error: No conversion from 'std::string' to 'int'
    false ? i : str1; // Error: No conversion from 'std::string' to 'int'
Advisement answered 27/4, 2022 at 19:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.