C++ Ternary operator ?: surprising conversion rules.

Started by
18 comments, last by alh420 9 years ago


What does that code print? Try to guess without running it...

At some point this feature of the language (combined with a couple of other unfortunate facts) cost me a week of cleaning up a mess.

I can't figure out whatever you're trying to illustrate. All I see is that the condition is true, so the zero has to be assigned to the double somehow. However the 0 gets there, your struct doesn't do anything sneaky with its value anyway, so I think it prints 0.

Is the condition supposed to be false?

Advertisement

Is the condition supposed to be false?

I assume it's supposed to be false, yes. The interesting part of that code happens when the false-expression is evaluated.

Is the condition supposed to be false?

I assume it's supposed to be false, yes. The interesting part of that code happens when the false-expression is evaluated.


Oooops! You are right: It was supposed to be false.


What does that code print? Try to guess without running it...

What does this do, and what is this called? I've never seen this before:


operator double() { return x; }
"I would try to find halo source code by bungie best fps engine ever created, u see why call of duty loses speed due to its detail." -- GettingNifty

What does this do, and what is this called? I've never seen this before:


operator double() { return x; }

It is operator overloading.

You are probably familiar with things overloading operators like operator+ or operator>>

You can overload many other operators.

That one is a conversion operator. You can provide custom conversions to things like doubles and floats, shorts and longs, and other types.

There are many infrequently overloaded operators out there. The function call operator, or operator(), is an interesting one used for fun effect. Some people are more familiar with that by making things called "functors" or "function objects", overloading operator() so you can call the function operation on an object. The memory management operators( new, new[], delete, delete[] ) can be overloaded, and that one is usually mentioned in books.

The comma operator or operator, can be overloaded, but since it has weird precedence it can make weird things happen and people don't tend to use it.

Smart pointers tend to overload operator&, operator*, and operator->, so you might be familiar with those. The member reference operators of operator-> and operator->* are sometimes overloaded as a pair which can do creepy but wonderful things if you are implementing a proxy object.

The subscript or array operator[] can be overloaded, which many people have used as a notation for n-dimensional arrays. Since you can overload the parameters as well, you can add two, three, four, or however many you want. That leads to things like bar=foo[x,y,true]; which can freak some people out.

Relatively new is operator"" or the literal operator and it has some potentially odd effects that people are still playing with, especially when it comes to templates. The literal operator lets you create your own custom suffixes, and the standard gives this example to help you understand it:

[Example:

long double operator "" _w(long double);
std::string operator "" _w(const char16_t*, std::size_t);
unsigned operator "" _w(const char*);
int main() {
  1.2_w;    // calls operator "" _w(1.2L)
  u"one"_w; // calls operator "" _w(u"one", 3)
  12_w;     // calls operator "" _w("12")
  "two"_w;  // error: no applicable literal operator
}
— end example ]

The only operators left out of the party are the member selection operators dot (.) and dot star (.*) which are needed to guarantee you can access the real item somehow, the scope resolution operator ( :: ) for hopefully obvious reasons, the conditional operator ( ?: ) for reasons covered in the discussion already, and the sizeof and typeid operators because those could cause even worse havoc if they were customized. But everything else can be customized with whatever you need.

Here's a much more subtle problem with the rules to determine the return type of the ternary operator:

...

Interestingly, the following code is ill-formed though:


#include <iostream>

struct X {
  double x;
  
  X(double x) : x(x) {
  }
  
  operator double() const {
    return x;
  }
};

int main() {
  X x(5.32);
  bool condition = false;
  double d = condition ? 0 : x;
  std::cout << d << '\n';
}

But reading the standard it all makes perfect sense (in the somewhat weird way that many of c++ constructs make sense).

openwar - the real-time tactical war-game platform

It might make sense in that somewhat weird way, but this is what actually bit me in the butt:
double limit_price = order.is_market() ? 0 : order.get_price();
Now, `Order::get_price()' used to return a `double', and the code did what I expected. Then someone defined a type `Price' with an implicit conversion to `double', and now the code starts to truncate my prices.

Two things I now do that would help:
(1) Use constants of the appropriate type (so `0.0' instead of `0' in this case).
(2) Run g++ with -Wconversion, which will warn about this one.

Here's a much more subtle problem with the rules to determine the return type of the ternary operator:


#include <iostream>

struct X {
  double x;
  
  explicit X(double x) : x(x) {
  }
  
  operator double() const {
    return x;
  }
};

int main() {
  X x(5.32);
  bool condition = false; // EDIT: Oooops! I originally wrote `true' here
  double d = condition ? 0 : x;
  std::cout << d << '\n';
}
What does that code print? Try to guess without running it...

At some point this feature of the language (combined with a couple of other unfortunate facts) cost me a week of cleaning up a mess.

I'm guessing 5 (haven't tested this yet). The explicit tag on the constructor prevents conversion of the 0 to an X, so we have to convert to the type of the second argument (the zero). But that's an integer literal, not a floating point or double one - so we convert x to double, then to integer, which rounds to 5.

This works with GCC, for reference:


class A
{
};

class B : public A
{
};

class C : public A
{
};

int main()
{
    A *a = false ? static_cast<A*>(new B()) : static_cast<A*>(new C());
}


This works with GCC, for reference:

It should work with any standard compliant compiler, since the expressions then will have the same type.

The OP wasn't really a problem, I just thought it was a bit peculiar before I understood the reasons behind it.

This was a good thread, from now I will be more careful with how I use it, making sure to always match types explicitly to avoid unexpected side effects.

This topic is closed to new replies.

Advertisement