📜 ⬆️ ⬇️

Secrets of the ternary operator

Every self-respecting C / C ++ programmer knows what a ternary operator is and most have 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 seemingly unrelated to its intended purpose, the possibilities in it lurk? This article gives you the opportunity to test your knowledge and, perhaps, learn something new.
Let's start with a little test.

Test


Does the following code compile? Explain why.
one.
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; 

four.
 return true ? 0 : 1; 

five.
 true ? return 0 : return 1; 


What will be the conclusion of the next piece? Why?
6
 std::cout << (false ? 9 : '9') << " " << (true ? 9 : '9'); 

')
What values ​​will a, b, and c have as a result of executing the following code? Why?
7
 int a = 1; int b = 1; int c = 1; a = true ? ++b : ++c; 

8. Name the situation where you cannot use if {...} else {...} , but you can use the ternary operator.
9. What potential dangers are hidden in using the ternary operator? What is their reason?
10. What unexpected uses of the ternary operator come to your mind?

Explanation


So, let's begin. The ternary operator is distinguished from a number of other operators in C ++. It is called " conditional expression ". Well, since this is an expression , an expression, then, like every expression, it must have a type and value category . Actually, by answering the questions what type and value category of ternary operators in each of the first seven questions of the test, we can easily solve the tasks.

This is where the fun begins. It turns out that the type of a ternary operator is the most common type of its two last operands. What does the most common mean? This is easiest to clarify with examples. Int and short will have an int .
A and B in the following snippet will also have an int .
 struct A{ operator int(){ return 1; } }; struct B{ operator int(){ return 3; } }; 

Those. the most common type is the type to which both operands can be cast. There may well be situations where there is no general type. For example,
 struct C{}; struct D{}; 

there is no general type, and the following fragment will not compile at all
 (true ? C() : D()); 

So. We dealt with the type of the ternary operator a bit. It remains to solve the issue with the value category . The following rule applies here: if a type conversion to the most general occurs in a ternary operator, the ternary operator is rvalue . If not, then lvalue . Now that we know what we know, we can easily answer the first 7 questions.

Answers


1. and 2. - Yes. Type conversion does not occur, and lvalue may well be assigned a value.
3. - No. Here is the type conversion. This means that the value category of the expression to the left of the "=" sign is rvalue . And rvalue , as you know, can not be assigned.
4. - Yes. We all did it more than once.
5. - No. The point here is that in C ++, the statement cannot break expression .
6. The program will display "57 9". In this fragment, due to the fact that the second and third operands are of different types, a conversion to the most general type occurs. In this case, int . A '9', as is known, has an ASCII code 57.
7. In this question lies another feature of the ternary operator. Namely, only the operand from the second and third, which reaches the thread of execution, is calculated. However, the same behavior can be observed in if {...} else {...}. Accordingly, the values ​​of the variables a, b and c will be 2, 2, 1.

Where it is impossible to use if {...} else {...}, but you can use the ternary operator?


For example, in the constructor initialization list. You cannot write like this:
 struct S { S() : if(true) i_(1) else i_(0){} int i_; }; 

But it can be done like this:
 struct S { S() : i_(some_condition ? 0 : 1){} int i_; }; 


When initializing the link depending on the condition. As you know, you cannot declare a non-initialized link, so the following fragment will not compile:
 int a = 3; int b = 4; int& i; if(some_condition) i = a; else i = b; 

But the following compiles successfully:
 int& i = (some_condition ? a : b); 


In C ++ 11, the ternary operator is used much more often. This is due to the fact that in constexpr functions there should be nothing but return `expression` . A `expression` may well be a ternary operator.
As an example, I will give a classic algorithm for determining the simplicity 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); } 

In the same example, by the way, one can see the use of cascade ternary operators, which can be of unlimited nesting and replace the multiple if {...} else {...}.

The dangers of a ternary operator


Suppose we have a class String
 class String { public: operator const char*(); }; 

And we can use it, for example, as follows:
 const char* s = some_condition ? "abcd" : String("dcba"); 

As we already know, the second and third operands of the ternary operator are reduced to the most general type. In this case, it is a const char * . But the String object (“dcba”) will be destroyed at the end of the expression and s will point to non-valid memory. At best, the program will crash if you try to use s later. At worst, it will produce incorrect results, causing discontent with the customer and a headache for the programmer.

"Unusual" use of the ternary operator


A ternary operator can be used to define a common type of two or more types. And this, in turn, can be used, for example, to determine whether one type is reduced to another.
 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 properties of the ternary operator, such use is almost obvious. What is unusual here, perhaps, is that it is not used for its intended purpose, i.e. not to select one of the two values ​​depending on the condition.

Source: https://habr.com/ru/post/205848/


All Articles