Every self-respecting C/C++ programmer knows what the ternary operator is, and most everyone used it at least once in their programs. But do you know all the secrets of the ternary operator? What potential dangers are associated with its use and what features, seemingly not related to its direct purpose, it has? This article gives you the opportunity to test your knowledge and maybe learn something new.

Let's start with a small test.

Test

Will the following code compile? Explain why.

1.

int i;
int j;
(false ? i: j) = 45;

2.

int i;
int j;
(true ? i: j) = 45;

3.

short i;
int j;
(true ? i: j) = 45;

4.

return true ? 0 : 1;

5.

true ? return 0 : return 1;

6. What will be the output of the following piece of code? Why?

std::cout << (false ? 9 : '9') << " " << (true ? 9 : '9');

7. What values will have variables a, b and c as a result of the following code execution? Why?

int a = 1;
int b = 1;
int c = 1;
a = true ? ++b : ++c;

8. Name a situation, in which you cannot useif{...} else{...}, but can use the ternary operator.

9. What are the potential dangers hiding in the use of the ternary operator What is the cause?

10. What unexpected use of the ternary operator comes to your mind?

Explanation

So, let’s start. The ternary operator stands out from other operators in C++. It is called the "conditional expression". Since it’s an expression, it should also have the type and value category, just like any other expression. Actually, if we answer the questions about the type and the value category of the ternary operator in each of the first seven questions of the test, it will be pretty easy to solve them.

Here comes the fun part. It turns out that the type of the ternary operator is the most common type of his last two operands. What does the most common mean? The easiest way to explain this is to use examples. The common type of int and short is int.

The common type of A and B in the next fragment of code is also int.

struct A{ operator int(){ return 1; } };
struct B{ operator int(){ return 3; } };

Thus, the most common type is the type, which both operands can be converted to. There can be situations when there’s no common type. For example,

struct C{};
struct D{};

have no common type, and the following code will not compile:

(true ? C() : D());

I guess the type of the ternary operator is clear now. Now, let’s talk about the value category. The following rule applies here: if types in the ternary operator are converted to the most common one, the ternary operator is rvalue. If not, it is lvalue. Knowing this, we can easily answer the first 7 questions.

Answers

1. and 2. — Yes, it will. Types are not converted, and it’s quite possible to assign a value to lvalue.

3. — No, it won’t compile. The conversion of types takes place here, which means that the value category of the expression on the left from "=" is rvalue. As you know, rvalue can not be assigned.

4. — Yes, it will. All of us have done so more than once.

5. — No, it won’t. The thing is that a statement cannot break an expression in С++.

6. The program will print “57 9”. Since the 2nd and the 3rd operands are of different types, the conversion to the most common type takes place here. In the given case, it is int. And, as we know, '9' has ASCII code 57.

7. Another feature of the ternary operator is hiding in this question. Namely, out of the second and the third operands, the only one is being computed — the operand reached by the execution thread.

However, the same behavior can be observed in if {...} else {...}. Thus, the values of variables a, b and с will be 2, 2, 1.

In what situations we can't use if {...} else {...}, but can use the ternary operator?

For instance, when initializing constructor. We cannot write the following:

struct S 
{
    S() : if(true) i_(1) else i_(0){}
    int i_;
};

But we can do it like this:

struct S 
{
    S() : i_(some_condition ? 0 : 1){}
    int i_;
};

That is to say, during the initialization of a reference depending on the condition. As we know, we cannot declare an uninitialized reference. Therefore, the following code fragment will not compile:

int a = 3;
int b = 4;
int& i;
if(some_condition)
  i = a;
else
  i = b;

While this fragment will compile successfully:

int& i = (some_condition ? a : b);

In C++11, the ternary operator is applied much more often. That's because the constexpr functions should have nothing but the return `expression`. While the `expression` may well be a ternary operator.

As an example, I’ll provide a classic algorithm for determining the primality of a number.

constexpr bool check_if_prime_impl(unsigned int num, unsigned int d)
{
  return (d * d > num) ? true : 
    (num % d == 0) ? false : 
      check_if_prime_impl(num, d + 1);
}
constexpr bool check_if_prime(unsigned int num)
{
  return (num <= 1) ? false : 
    check_if_prime_impl(num, 2);
}

By the way, the same example shows the use of nested ternary operators, where multiple ternary evaluations are also possible, so that to avoid multiple if {...} else {...} (don't drink too much kool-aid — for the sake of readability, if {…} else {…} might be a better choice).

Dangers of the Ternary Operator

Suppose we have a String class

class String
{
  public:
  operator const char*();
};

and we can use it like this:

const char* s = some_condition ? "abcd" : String("dcba");

As we already know, the second and third operands of the ternary operator are converted to the most common type. In this case, it is const char*. But the String(«dcba») object will be destroyed at the end of the expression, and s will point to the invalid memory. In the best case, the program will crash trying to use s later. In the worst case, it will produce wrong results, causing dissatisfaction among customers and a headache for the programmer.

An “Unusual” Use of the Ternary Operator

We can use the ternary operator to determine the common type of two and more types. This can also be used to determine whether one type is converted to the other one.

template <typename T, typename U>
struct common_type
{
    typedef decltype(true ? std::declval<T>() : std::declval<U>()) type;
};
template<typename T, typename U>
struct is_same{ enum { value = false; } };
template<typename T>
struct is_same<T, T>{ enum { value = true; } };
int main() 
{
  std::cout << is_same<int, common_type<A, B>::type>::value <<std::endl;
}

In fact, if you know the features of the ternary operator, such usage almost suggests itself. The unusual thing here is perhaps that it is not used for the intended purpose, i.e. not for selecting one value out of two, depending on the condition.

  • 0

Write your own articles at Kukuruku Hub

2 comments

Mironov Andrey
The following rule applies here: if types in the ternary operator are converted to the most common one, the ternary operator is rvalue.
Imagine a situation where both types can be converted to some common lvalue reference type. I speak types with user-defined conversion operators.
Gabriel
struct A { operator int() { return 1; } };
struct B { operator int() { return 3; } };

int main()
{
A a;
B b;
true? a: b;
}

Visual C++ 2015 refuses to compile the previous snippet and the error is: no conversion from 'B' to 'A'
Both gcc and clang compile successfully and the result is as expected.

Which in wrong? Visual C++ or gcc+clang?

Read Next