views:

482

answers:

4
class Foo {
  public:
  explicit Foo(double item) : x(item) {}

  operator double() {return x*2.0;}

  private:
  double x;
}

double TernaryTest(Foo& item) {
  return some_condition ? item : 0;
}

Foo abc(3.05);
double test = TernaryTest(abc);

In the above example, why is test equal to 6 (instead of 6.1) if some_condition is true?

Changing the code like below returns value of 6.1

double TernaryTest(Foo& item) {
  return some_condition ? item : 0.0; // note the change from 0 to 0.0
}

It seems that (in the original example) the return value from Foo::operator double is cast to an int and then back to a double. Why?

+1  A: 

The type of the ternary expression is determined at compile-time; it doesn't matter what some_condition is at runtime.

I guess the question is then: why does the compiler choose int instead of double in the first example?

Ben M
Indeed, the condition is irrelevant. Thanks for clarifying the question.
Chris Bednarski
+7  A: 

The conditional operator checks conversions in both directions. In this case, since your constructor is explicit (so the ?: is not ambiguous), the conversion from Foo to int is used, using your conversion function that converts to double: That works, because after applying the conversion function, a standard conversion that converts the double to int (truncation) follows. The result of ?: in your case is int, and has the value 6.

In the second case, since the operand has type double, no such trailing conversion to int takes place, and thus the result type of ?: has type double with the expected value.

To understand the "unnecessary" conversions, you have to understand that expressions like your ?: are evaluated "context-free": When determining the value and type of it, the compiler doesn't consider that it's the operand of a return for a function returning a double.


Edit: What happens if your constructor is implicit? The ?: expression will be ambiguous, because you can convert an int to an rvalue of type Foo (using the constructor), and a Foo to an rvalue of type int (using the conversion function). The Standard says

Using this process, it is determined whether the second operand can be converted to match the third operand, and whether the third operand can be converted to match the second operand. If both can be converted, or one can be converted but the conversion is ambiguous, the program is ill-formed.


Paragraphs explaining how your Foo is converted to int:

5.16/3 about condition ? E1 : E2:

Otherwise, if the second and third operand have different types, and either has (possibly cv-qualified) class type, an attempt is made to convert each of those operands to the type of the other. [...] E1 can be converted to match E2 if E1 can be implicitly converted to the type that expression E2 would have if E2 were converted to an rvalue (or the type it has, if E2 is an rvalue).

4.3 about "implicitly converted":

An expression e can be implicitly converted to a type T if and only if the declaration T t = e; is well-formed, for some invented temporary variable t.

8.5/14 about copy initialization ( T t = e; )

If the source type is a (possibly cv-qualified) class type, conversion functions are considered. The applicable conversion functions are enumerated (13.3.1.5), and the best one is chosen through overload resolution (13.3). The user-defined conversion so selected is called to convert the initializer expression into the object being initialized. If the conversion cannot be done or is ambiguous, the initialization is ill-formed.

13.3.1.5 about the conversion function candidates

The conversion functions of S and its base classes are considered. Those that are not hidden within S and yield type T or a type that can be converted to type T via a standard conversion sequence (13.3.3.1.1) are candidate functions.

Johannes Schaub - litb
Any way around this without making the Foo constructor implicit?
Chris Bednarski
Sure - make the types the same explicitly. As you've seen, making the numeric literal into a double makes it work the way you want.
David Thornley
I mean the truncation is not very obvious after having a quick glance at the code. Having an implicit Foo construcor fixes the problem. So does the 0.0 and so does return condition ? item : Foo(0);
Chris Bednarski
Having the constructor implicit will make the `?:` expression ambiguous. Why don't you simply write `0.0`?
Johannes Schaub - litb
@litb: it doesn't. Making the constructor implicit means that ?: evaluates to Foo, bacuase an int can then be converted by Foo(double)
Chris Bednarski
@KNoodles, what compiler do you use?
Johannes Schaub - litb
which conversion function converts a Foo to an rvalue of type int?microsoft compiler
Chris Bednarski
Your conversion function returns `double`, but `13.3.1.5` will also consider conversion functions whose result can be further converted to `int` (as is the case here) - so in the end, your `Foo` is implicitly convertible to `int`. It sounds like your compiler is defect, if it doesn't at least emit a warning.
Johannes Schaub - litb
Wouldn't surprise me at all if this compiler was not standard compliant. I've accepted this answer
Chris Bednarski
@litb: I'm convinced that the compiler should emit a warning for a possble loss of data. I'm still not convinced about the ambiguity of the type, howerver. The temporary Foo(0) is an lvalue, which means that we should be looking at the first part of 5.16/3.
Chris Bednarski
No, `Foo(0)` is an rvalue. Don't be fooled that Visual C++ accepts `Foo `: That's non-standard, and if you put the warning level up, then you will see it warn. For the standardese: See 5.2.3/1: "A simple-type-specifier (7.1.5) followed by a parenthesized expression-list constructs a value of the specified type given the expression list. If the expression list is a single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression (5.4)." ...
Johannes Schaub - litb
... and `5.4`: "The result of the expression (T) cast-expression is of type T. The result is an lvalue if T is a reference type, otherwise the result is an rvalue.". It's actually one of the best examples of an rvalue, because it's both unnamed and living for only a short time frame.
Johannes Schaub - litb
The compiler is not required, though, to emit a warning for a loss of data. It should however emit at least a warning for an incorrect program. At the Standard level, there is no difference between an error message and a warning - that's why i said it should at least warn for the invalid conditional expression - actually in practice it should give an error.
Johannes Schaub - litb
The part where it says "If E2 is an rvalue, or if the conversion above cannot be done..." does only mean that if the first part doesn't work for `E1 -> E2`, then other ways are tried to convert `E1` -> `E2`. It doesn't say anything about conversion of `E2` to `E1`. So, even if `Foo(0)` (would be `E1` -> `E2` using the first part) would work according to the first part, it will *still* check conversions for `E2` -> `E1` and we get ambiguity because we could convert `E2` to an lvalue of `E1` and also `E1` to an rvalue of `E2`.
Johannes Schaub - litb
If you wish, i can elaborate more in the answer. There's only little space in comments :)
Johannes Schaub - litb
I based my assumption on a slightly different criteriareturn some_condition ? item : Foo(0) = Foo(1);
Chris Bednarski
In C++, "lvalue" doesn't mean "can be assigned to". It rather means "has a storage location somewhere". For example, in `int const A = 1;`, `A` is an lvalue because it's stored somewhere, even though you cannot assign to it.
Johannes Schaub - litb
Here is an answer about lvalues and rvalues in C++: http://stackoverflow.com/questions/579421/often-used-seldom-defined-terms-lvalue/579514#579514
Johannes Schaub - litb
It does seem like this is an implementation defined behavior. Foo(1.2) = Foo(3.2); compiles fine without any warnings. Foo(1.2) being an rvalue should not be allowed to be used as a lvalue
Chris Bednarski
@KNoodles, you should ask this as a separate question. That should really be answered as a separate answer to a separate question - not in these comments.
Johannes Schaub - litb
I don't know where my head was with the last comment. Foo x(3.2); true ? x : ( does produce a warning as well. Microsoft calls this use of an rvalue a non-standard extension. Thanks for your in-depth explanations litb
Chris Bednarski
A: 

ternary operator guesses the type from its arguments. it cannot convert item to int but it can convert item to double which it then converts to int.

codymanix
+7  A: 

This is covered in positively confusing detail in section 5.16 of the standard. The important part is in paragraph 3. "If E2 is an lvalue: E1 can be converted to match E2 if E1 can be implicitly converted (clause 4) to the type 'reference to T2', subject to the constraint that in the conversion the reference must bind directly (8.5.3) to E1."

In the expression, the only lvalue is item, so the question is whether 0 (an int) can be implicitly converted to type Foo. In this case, there is no implicit conversion of any other type to a Foo, since the only available conversion function is marked explicit. Therefore, that doesn't work, and we follow with "if E2 is an rvalue, or if the conversion above cannot be done:" (skipping the part about if they both have class type) "Otherwise (i.e., if E1 or E2 has a nonclass type, or if they both have class types but the underlying classes are not either the same or one a base class of the other): E1 can be converted to match E1 if E1 can be implicitly converted to the type that expression E2 would have if E2 were converted to an rvalue (or the type it has, if E2 is an rvalue)."

Therefore, we see that 0 is an rvalue, of type int. We can convert a Foo, since we can implicitly convert a Foo to a double and thence to an int. Then:

"Using this process, it is determined whether the second operand can be converted to match the third operand, and whether the third operand can be converted to match the second operand. If both can be converted, oor one can be converted but the conversion is ambiguous, the program is ill-formed. If neither can be converted, the operands are left unchanged and further checking is performed as described below. If exactly one conversion is possible, that conversion is applied to the chosen operand and the converted operand is used in the place of the original operand for the remainder of this section."

Since we can convert a Foo to an int, we convert the Foo to an int for the remainder of the determination. We've now got two ints as expression types, and at least one is an rvalue.

I can go on with paragraph 5 and 6, but I think it's pretty obvious that the expression has type int.

I think the takeaways are:

  1. Your compiler is functioning according to the standard.

  2. The rules on the type of a conditional expression are too complicated to be easily learned. Don't push the envelope, because you'll make a mistake sometime. (Besides, this is exactly the sort of place where a compiler might fail to implement the standard precisely.)

  3. Try to specify types so that both the second and third expression are of the same type. In any case, try to avoid expressions that are not of the desired type.

David Thornley
+1 for quoting (and explaining!) the standard.
Bill
Thanks. this is the kind of detail I was after.
Chris Bednarski
The first block, where it requires conversion to `reference to T2` from `E1` doesn't apply - because `Foo ` will never work, even with an implicit constructor of `Foo`. Furthermore, the constraint is that the reference binds directly to `E1`. This only works for an `E1` that is converted by a conversion function `operator Foo` to `reference to Foo` (see 8.5.3/5).
Johannes Schaub - litb
@litb: Thanks for the clarification.
David Thornley