The Misunderstood Conditional (Ternary) Operator
Most languages that come from a C background include a single conditional ternary operator with a syntax of exp ? true_val : false_val
. This operator is often maligned and it is common to see its use prohibited in coding standards. Like all programming language features it can be abused at times but it absolutely has its place in well-written code.
Other Languages
The idea of conditional operator is not unique to C and is common in functional programming languages. Some of the dislike for the conditional operator stems from preferring statement-based programs over expression ones. So the "if" statement feels just right and simple to someone thinking step-by-step when writing a procedural program. However, a conditional operator feels right at home to someone writing in a functional language. This is actually a good argument to avoid it in C code. C programmers don't generally write code doing a lot of complex work in expressions.
It is important to realize that dislike for this operator is a stylistic and background preference and not something inherently bad about its use. In this post, I provide an example of the use of the ternary operator that I think should both be comfortable to procedural programs as well as fitting an important need that other C constructs don't handle well.
Lisp
In Lisp, the equivalent of the conditional operator is the "if construct" (if (exp) (true_action) (false_action))
. Lisp isn't a pure functional language so this is often used in the same way the C "if statement" is. The major difference is that this is actually an expression so the true and false condition values are what the expression is evaluated to. This means it can be assigned to variables or passed to functions exactly the same way the C conditional operator can. So while it is named "if", it has way more in common with the conditional operator than a C "if" statement.
F#
F# continues the trend of having "if" be an expression.
if exp then true_value
else false_value
Like lisp this is often used like an "if" statement is, however it has a value and can be used like any other value or expression.
Bad Usage Patterns
C expressions can be quite powerful and contain things that can be confusing and unexpected. People learning C for the first time all run into the "=" being an expression. This means you can have code like if (a = 1)
be completely valid but totally the wrong. Also, the ability for integers and pointers to be treated as boolean can cause for confusing expressions. This allows for some nice terse usage patterns but runs the risk of getting too clever and being confusing. Of course operator precedence is also a common trap. When combined with these other behaviors of C expressions, conditional operators can be used to make them even harder to understand.
When trying to fix non-working code, it is easy to just slip in a conditional operator to try to fix it in as few changes as possible.
int count = strlen(a);
Later on becomes
int count = a ? strlen(a) : 0;
Which later on becomes
int count = a ? (strcmp(a, "undefined") ? strlen(a) : -1) : 0;
It isn't hard to see how this can quickly slip in everywhere in the code making things a mess. Keep in mind, this is just bad usage of the ternary operator. Anything can be abused and it isn't intrinsically bad. As mentioned above, it is often used in other programming languages without issues.
Let's Do This Right!
Ok so how do we use the conditional operator in a way that isn't confusing and fits in well with C-derived languages? The conditional operator can help with some usage patterns where other language features don't work as well or have limitations. I'm going to present an example of how this operator can be used as a better alternative to a switch statement.
Switch / Case
The switch statement is one of the odder programming language constructs in C. Being able to map one value to another one or to an action is a common need. But the switch suffers from odd syntax, needs lots of "break" statements and is a little tricky and error-prone.
Here is a simple example of a switch statement
int result;
switch (val)
{
case 0 :
result = 5;
break;
case 1 :
result = 20;
break;
case 2 :
result = 100;
break;
}
This has a myriad of issues:
- It is verbose; the extra "break" statements, the curly braces all make a simple numeric mapping large.
- It is easy to miss a "break" statement. This is such a common mistake that C# goes to far as to require a "break" statement or the code won't compile.
- The type of variable you switch on must be integral or an enum. This rules out doing string comparisons or comparing on other common data types. This is very constraining when coming from other programming languages that don't have these limitations.
- The values you compare with case must be constants.
- In this example, it isn't possible to make the "result" variable const. Not everyone values using "const" as much as I do, but for those of us who do, this is a big deal.
- There are no compiler checks that "result" has been defined by one of the case blocks. Omitting the default block is fully allowed. It is also possible to have the case blocks forget to assign a value to "result".
So here is a nice alternative
const int result = val == 0 ? 5 :
val == 1 ? 20 :
val == 2 ? 100 :
(assert(false), 0);
- This code is much smaller than the switch statement
- The formatting clearly shows it is a mapping from the left to the right value. One can view it as
0 => 5
,1 => 20
,2 => 100
and it isn't too far off from the actual code syntax. - The code can use const if desired (yes please!)
- You will get a compile-time error if you don't specify a default value.
- This pattern works with types other than integers and works with comparisons other than simple value equality.
The formatting of this code is critical. Without the spacing and alignment this can be formatted to look confusing. I have also seen people format something similar except with the ":" on the start of the following line rather than at the end of the current line. This is just as clear but I, personally, prefer it on the end of the same line.
I can understand objections to the use of the comma operator with the assert. It is a bit of a trade-off. For those who don't know, the assert will be evaluated and the "result" expression will get the value of 0. This is just my little touch and isn't required. It is possible to simply have a valid default value. Another nice option (in C++) is to call a templated function that throws an exception. The template is needed to be able to write one function to match any expression type. Just keep in mind that the comma trick isn't required and shouldn't be an objection to the overall pattern; I wanted to show how flexible it can be.
One downside of this pattern compared to the switch statement is usually compilers can use a jump table to optimize the switch statement. This is why the switch only supports integers. In theory, a compiler could use one here as well, but it is a harder optimization to make. So this is a case where performance could be slightly impacted in exchange for code clarity.
Unlike switch statements, this doesn't have the ability to fall through the case blocks. I don't think this is usually a problem. As noted, the C# language requires "break" statements as fall-through isn't supported. It isn't something that I usually miss but am noting it as it is something switch has the upper-hand on — when done intentionally at least.
Conclusion
While not perfect, I think the example I show is a good use of the conditional operator. It is clear, flexible, and reasonably simple. I don't remember where I have seen code similar to this before but there aren't a lot of examples of it online. I think this is a great use of the conditional operator and encourage it to be used instead of switch statements or "if" chains where it makes sense. Even for the unconvinced, hopefully this at least showed some merit for this technique and that conditional operators don't have to be contributing to confusing code.