Implicit Conversion Issue in a Ternary Condition

Implicit conversion issue in a ternary condition

This is a consequence of the confluence of two characteristics of C#.

The first is that C# never "magics up" a type for you. If C# must determine a "best" type from a given set of types, it always picks one of the types you gave it. It never says "none of the types you gave me are the best type; since the choices you gave me are all bad, I'm going to pick some random thing that you did not give me to choose from."

The second is that C# reasons from inside to outside. We do not say "Oh, I see you are trying to assign the conditional operator result to an ILogger; let me make sure that both branches work." The opposite happens: C# says "let me determine the best type returned by both branches, and verify that the best type is convertible to the target type."

The second rule is sensible because the target type might be what we are trying to determine. When you say D d = b ? c : a; it is clear what the target type is. But suppose you were instead calling M(b?c:a)? There might be a hundred different overloads of M each with a different type for the formal parameter! We have to determine what the type of the argument is, and then discard overloads of M which are not applicable because the argument type is not compatible with the formal parameter type; we don't go the other way.

Consider what would happen if we went the other way:

M1( b1 ? M2( b3 ? M4( ) : M5 ( ) ) : M6 ( b7 ? M8() : M9() ) );

Suppose there are a hundred overloads each of M1, M2 and M6. What do you do? Do you say, OK, if this is M1(Foo) then M2(...) and M6(...) must be both convertible to Foo. Are they? Let's find out. What's the overload of M2? There are a hundred possibilities. Let's see if each of them is convertible from the return type of M4 and M5... OK, we've tried all those, so we've found an M2 that works. Now what about M6? What if the "best" M2 we find is not compatible with the "best" M6? Should we backtrack and keep on re-trying all 100 x 100 possibilities until we find a compatible pair? The problem just gets worse and worse.

We do reason in this manner for lambdas and as a result overload resolution involving lambdas is at least NP-HARD in C#. That is bad right there; we would rather not add more NP-HARD problems for the compiler to solve.

You can see the first rule in action in other place in the language as well. For example, if you said: ILogger[] loggers = new[] { consoleLogger, suppressLogger }; you'd get a similar error; the inferred array element type must be the best type of the typed expressions given. If no best type can be determined from them, we don't try to find a type you did not give us.

Same thing goes in type inference. If you said:

void M<T>(T t1, T t2) { ... }
...
M(consoleLogger, suppressLogger);

Then T would not be inferred to be ILogger; this would be an error. T is inferred to be the best type amongst the supplied argument types, and there is no best type amongst them.

For more details on how this design decision influences the behaviour of the conditional operator, see my series of articles on that topic.

If you are interested in why overload resolution that works "from outside to inside" is NP-HARD, see this article.

Strange implicit conversions with the ternary operator

TL;DR; clang is right, since there are no possible conversions between A and B, overload resolution is used to determine the conversions to be applied to the operands, and the following (fictive) overloaded operator is selected:

int operator?:(bool, int, int);

There exists such (again, fictive) overload of the ?: operator for any pair of arithmetic types (see references below).


Standard rules:

Since you cannot convert A to B or B to A, then the following applies:

[expr.cond]

Otherwise, the result is a prvalue.
If the second and third operands do not have the same type, and either has (possibly cv-qualified) class type, overload resolution is used to determine the conversions (if any) to be applied to the operands ([over.match.oper], [over.built]).
If the overload resolution fails, the program is ill-formed.
Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this subclause.

This falls back to this:

[over.match.oper]

If either operand has a type that is a class or an enumeration, a user-defined operator function might be
declared that implements this operator or a user-defined conversion can be necessary to convert the operand
to a type that is appropriate for a built-in operator.

[...]

The set of candidate functions for overload resolution is the union of the member candidates, the non-member candidates, and the built-in candidates.

If a built-in candidate is selected by overload resolution, the operands of class type are converted to the types of the corresponding parameters of the selected operation function, except that the second standard conversion sequence of a user-defined conversion sequence is not applied.
Then the operator is treated as the corresponding built-in operator and interpreted according to [expr.compound].

In your case, there is a built-in candidate:

[over.built#27]

For every pair of promoted arithmetic types L and R, there exist candidate operator functions of the form

LR      operator?:(bool, L, R);

where LR is the result of the usual arithmetic conversions ([expr.arith.conv]) between types L and R.
[ Note: As with all these descriptions of candidate functions, this declaration serves only to describe the built-in operator for purposes of overload resolution. The operator “?:” cannot be overloaded. — end note ]


Extra details:

Since the ?: operator cannot be overloaded, this means that your code only works if both types can be converted to an arithmetic type (e.g., int). As a "counter"-example, the following code is ill-formed:

struct C { };
struct A { operator C() const; };
struct B { operator C() const; };

auto c = true ? A{} : B{}; // error: operands to ?: have different types 'A' and 'B'

Also note that you would get an ambiguous "call" if one of the type is convertible to two different arithmetic types, e.g., int and float:

struct A { operator int() const; };
struct B {
operator int() const;
operator float() const;
};

auto c = true ? A{} : B{};

The error (from gcc) is actually full of information:

error: no match for ternary 'operator?:' (operand types are 'bool', 'A', and 'B')

auto c = true ? A{} : B{};
~~~~~^~~~~~~~~~~

  • note: candidate: operator?:(bool, float, int) <built-in>
  • note: candidate: operator?:(bool, float, float) <built-in>

Nested Ternary Implicit type Conversion issue: Type of conditional expression cannot be determined because there is no implicit conversion between

First of all, do listen to the commenters. Switch/case really does seem like it would result in more readable, maintainable code.

Second of all, what you are trying to do in your second code snippet is set your property changed handler to the result of subscribing to a handler, which is void.

Specifically, _attributeTableConfigs.PropertyChanged += new PropertyChangedEventHandler(appTableConfigs_PropertyChanged) does not return a value--it returns void. So, you're assignment is not possible.

You must do what you're trying to do in separate steps.

For example:

PropertyChangedEventHandler handler;
switch (tableType)
{
case TableType.Attribute:
handler = new PropertyChangedEventHandler(appTableConfigs_PropertyChanged);
_attributeTableConfigs.PropertyChanged += handler;
break;
case TableType.Core:
handler = new PropertyChangedEventHandler(appTableConfigs_PropertyChanged);
_coreTableConfigs.PropertyChanged += handler;
break;
case TableType.Domain:
handler = new PropertyChangedEventHandler(appTableConfigs_PropertyChanged);
_domainTableConfigs.PropertyChanged += handler;
break;
case TableType.Configuration:
handler = new PropertyChangedEventHandler(appTableConfigs_PropertyChanged);
_configTableConfigs.PropertyChanged += handler;
break;
default:
handler = new PropertyChangedEventHandler(appTableConfigs_PropertyChanged);
_offlineTableConfigs.PropertyChanged += handler;
break;
}

Or, if the handler is always the same:

PropertyChangedEventHandler handler = appTableConfigs_PropertyChanged;
switch (tableType)
{
case TableType.Attribute:
_attributeTableConfigs.PropertyChanged += handler;
break;
case TableType.Core:
_coreTableConfigs.PropertyChanged += handler;
break;
case TableType.Domain:
_domainTableConfigs.PropertyChanged += handler;
break;
case TableType.Configuration:
_configTableConfigs.PropertyChanged += handler;
break;
default:
_offlineTableConfigs.PropertyChanged += handler;
break;
}

Implicit cast to void* in ternary operator?

The standard explicitly says that if both operands of the conditional operator are pointers and one is void* (not including a null-pointer constant), then the result is void*. From C11 6.5.15/6:

If both the second and third operands are pointers or one is a null pointer constant and the other is a pointer, the result type is a pointer to a type qualified with all the type qualifiers of the types referenced by both operands. Furthermore, if both operands are pointers to compatible types or to differently qualified versions of compatible types, the result type is a pointer to an appropriately qualified version of the composite type; if one operand is a null pointer constant, the result has the type of the other operand; otherwise, one operand is a pointer to void or a qualified version of void, in which case the result type is a pointer to an appropriately qualified version of void.

C++ : Ternary Operator (Conditional Operator) and its Implicit Type Conversion Rules

Quoting MSDN:

Conditional expressions have right-to-left associativity. The first
operand must be of integral or pointer type. The following rules apply
to the second and third operands:

If both operands are of the same type, the result is of that type.

If both operands are of arithmetic or enumeration types, the usual arithmetic conversions (covered in Arithmetic Conversions) are
performed to convert them to a common type.

If both operands are of pointer types or if one is a pointer type and the other is a constant expression that evaluates to 0, pointer
conversions are performed to convert them to a common type.

If both operands are of reference types, reference conversions are performed to convert them to a common type.

If both operands are of type void, the common type is type void.

If both operands are of the same user-defined type, the common type is that type.

If the operands have different types and at least one of the operands has user-defined type then the language rules are used to
determine the common type. (See warning below.)

Basically what happens is that C++ compiler looks for common type for second and third operand. If it can find it, that's result type. If it can't find it, it results in a compilation time error.

If you want to see standard position, you can see the rules in working draft for newest standard, 5.16 (page 129).

As of not converting int to pointclass - general rule is that you always go down the hierarchy, not up - imagine more advanced class hierarchy; somewhere up there could be dozens of ways to convert both types up to some other class, but is that really what you want? Moreover, determining which class to use could be impossible. Therefore, we downcast.

No implicit conversion when using conditional operator

This is the expected behavior.

Since no implicit conversion exists between X and Y (even if they share a common base, there is no implicit conversion between them), you need to explicitly cast (at least) one of them to the base class so that an implicit conversion exists.

A detailed explanation from the C# specification:

The second and third operands of the ?: operator control the type of the conditional expression. Let X and Y be the types of the second and third operands. Then,

If X and Y are the same type, then this is the type of the conditional expression.

Otherwise, if an implicit conversion (Section 6.1) exists from X to Y, but not from Y to X,
then Y is the type of the conditional expression.

Otherwise, if an implicit conversion (Section 6.1) exists from Y to X, but not from X to Y,
then X is the type of the conditional expression.

Otherwise, no expression type can be determined, and a compile-time error occurs.

Strange behaviour in implicit conversion if inline if used

Do you have any idea about this behaviour ?

Absolutely. The type of a conditional operator expression must either be the type of the second operand or the type of the third operand. (And if those two types aren't the same, exactly one of the types has to be implicitly convertible to the other.) The compiler doesn't try to find a "lower common denominator" type, and the use of the result isn't important either (the compiler doesn't "notice" that you're assigning the result to a variable of type A).

You can fix this yourself by just explicitly casting either operand:

A myObject = condition ? (A) new B() : new C();

or

A myObject = condition ? new B() : (A) new C();

Note that this isn't limited to user-defined conversion operators; the same is true for simple reference conversions based on derived classes:

Button x = new Button();
String y = "foo";
object z = condition ? x : y; // Compile-time error

See section 7.14 of the C# specification for more details.

Cannot be determined because there is no implicit conversion with ternery if return

You need to explicitly cast the result to IHttpActionResult:

return deleted ? (IHttpActionResult) this.Ok() : this.NotFound();

Edit:

As for Grants question:

Why does Sam's second block of code work without explicitly casting to
IHttpActionResult, just out of curiosity? Is this something particular
to the conditional ?: operator?

Lets create a simple demonstration.
Assume the following code:

public interface IFoo { }

public class B : IFoo { }

public class C : IFoo { }

And then the following:

public class A
{
IFoo F(bool b)
{
return b ? (IFoo) new B() : new C();
}
}

Lets see how the compiler de-compiles the ternary operator:

private IFoo F(bool b)
{
IFoo arg_13_0;
if (!b)
{
IFoo foo = new C();
arg_13_0 = foo;
}
else
{
arg_13_0 = new B();
}
return arg_13_0;
}

The explicit cast is enough for the compiler to infer that the variable should be of type IFoo and hence satisfy our whole if-else. That is why it is enough for us to "hint" the compiler only once of our type cast.

@dcastro has referenced the exact part of the language specification which determines the control of the type, see that for text-book definition.



Related Topics



Leave a reply



Submit