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 direct purpose, are the possibilities lurking in it? This article gives you the opportunity to test your knowledge and possibly learn something new.
Let's start with a little test.
Will the following code compile? Explain why.
one.
2.
3.
4.
5.
What will be the conclusion of the next piece? Why?
6.
What are the values of variables a, b and c as a result of the following code? Why?
7.
8. What is the situation where you cannot use if {...} else {...} , but you can use the ternary operator.
9. What are the potential dangers in using the ternary operator? What is their reason?
10. What unexpected uses of the ternary operator come to your mind?
So, let's begin. The ternary operator stands out from a number of other operators in C ++. It is called a " conditional expression ". Well, since this is expression , an expression, then like every expression, it must have a type and value category . Actually, 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 problems posed.
Here the fun begins. It turns out that the type of ternary operator will be the most general type of its last two operands. What does the most common mean ? This is most easily explained with examples. We int and short common type is int.
In A and B in the next fragment, the int type will also be the common type .
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,
there is no general type, and the next fragment does not compile at all
So. We sorted out the type of ternary operator a bit. It remains to resolve the issue with value category . Here the following rule applies: if the type is converted to the most common in the ternary operator, then the ternary operator is rvalue . If not, then lvalue . Now that we know what we know, we can easily answer the first 7 questions.
1. and 2. - Yes. Type conversion does not occur, and lvalue is quite possible to assign a value.
3. - No. Type conversion takes place here. So the value category of the expression to the left of the "=" sign is rvalue . And rvalue , as you know, cannot be assigned.
4. - Yes. We have all done this more than once.
5. - No. Here the whole point is that in C ++ statement cannot break expression .
6. The program displays "57 9". In this fragment, due to the fact that the 2nd and 3rd operand have different types, a conversion to the most general type occurs. In this case, int . A '9', as you know, has ASCII code 57.
7. In this question lies another feature of the ternary operator. Namely, only that operand from the second and third, to which the thread of execution reaches, is calculated. However, the same behavior can be observed with if {...} else {...}. Accordingly, the values of the variables a, b and c will be 2, 2, 1.
For example, in the constructor initialization list. You cannot write like this:
But it is quite possible like this:
When initializing the link, depending on the condition. As you know, you cannot declare an uninitialized link, so the following fragment does not compile:
But the following compiles successfully:
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` . And `expression` may well be a ternary operator.
As an example, I will give a classical algorithm for determining the simplicity of a number
In the same example, by the way, you can see the use of cascading ternary operators, which can be unlimited nesting and replace multiple if {...} else {...}.
Let's say we have a class String
And we can use it, for example, like this:
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 const char * . But the String object ("dcba") is destroyed at the end of the expression and s will point to invalid memory. In the best case, the program will crash when you try to use s later . At worst, it will produce incorrect results, causing dissatisfaction with the customer and a headache for the programmer.
The ternary operator can be used to determine the general type of two or more types. And this, in turn, can be used, for example, to determine whether one type is converted to another.
Let's start with a little test.
Test
Will 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;
4.
return true ? 0 : 1;
5.
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 are the values of variables a, b and c as a result of the following code? Why?
7.
int a = 1;
int b = 1;
int c = 1;
a = true ? ++b : ++c;
8. What is the situation where you cannot use if {...} else {...} , but you can use the ternary operator.
9. What are the potential dangers 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 stands out from a number of other operators in C ++. It is called a " conditional expression ". Well, since this is expression , an expression, then like every expression, it must have a type and value category . Actually, 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 problems posed.
Here the fun begins. It turns out that the type of ternary operator will be the most general type of its last two operands. What does the most common mean ? This is most easily explained with examples. We int and short common type is int.
In A and B in the next fragment, the int type will also be the common type .
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 next fragment does not compile at all
(true ? C() : D());
So. We sorted out the type of ternary operator a bit. It remains to resolve the issue with value category . Here the following rule applies: if the type is converted to the most common in the ternary operator, then the ternary operator is rvalue . If not, then lvalue . Now that we know what we know, we can easily answer the first 7 questions.
The answers
1. and 2. - Yes. Type conversion does not occur, and lvalue is quite possible to assign a value.
3. - No. Type conversion takes place here. So the value category of the expression to the left of the "=" sign is rvalue . And rvalue , as you know, cannot be assigned.
4. - Yes. We have all done this more than once.
5. - No. Here the whole point is that in C ++ statement cannot break expression .
6. The program displays "57 9". In this fragment, due to the fact that the 2nd and 3rd operand have different types, a conversion to the most general type occurs. In this case, int . A '9', as you know, has ASCII code 57.
7. In this question lies another feature of the ternary operator. Namely, only that operand from the second and third, to which the thread of execution reaches, is calculated. However, the same behavior can be observed with if {...} else {...}. Accordingly, the values of the variables a, b and c will be 2, 2, 1.
Where can not I use if {...} else {...}, but can I 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 is quite possible 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 an uninitialized link, so the following fragment does 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` . And `expression` may well be a ternary operator.
As an example, I will give a classical 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, you can see the use of cascading ternary operators, which can be unlimited nesting and replace multiple if {...} else {...}.
Dangers of ternary operator
Let's say we have a class String
class String
{
public:
operator const char*();
};
And we can use it, for example, like this:
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 const char * . But the String object ("dcba") is destroyed at the end of the expression and s will point to invalid memory. In the best case, the program will crash when you try to use s later . At worst, it will produce incorrect results, causing dissatisfaction with the customer and a headache for the programmer.
Unusual use of the ternary operator
The ternary operator can be used to determine the general type of two or more types. And this, in turn, can be used, for example, to determine whether one type is converted to another.
template
struct common_type
{
typedef decltype(true ? std::declval() : std::declval()) type;
};
template
struct is_same{ enum { value = false; } };
template
struct is_same{ enum { value = true; } };
int main()
{
std::cout << is_same::type>::value <
На самом деле, если знать свойства тернарного оператора, такое использование практически напрашивается само собой. Необычным здесь, пожалуй является лишь то, что он используется не по прямому назначению, т.е. не для выбора одного значения из двух в зависимости от условия.