Ternary Operator in C (?:): Syntax, Examples and the Traps

Only one branch runs, but both decide the type — and that is where printf goes wrong. Tested C examples, plus the MAX macro trap.

A path forking into two branches with only one taken, both leading to a single result, representing the C ternary operator

Only one branch of a ternary ever runs. Both branches decide its type. That single asymmetry is why printf("%d", flag ? 1 : 2.0) compiles, prints garbage, is undefined behaviour, and is the most common way C programmers get burned by an operator they think they understand.

This guide covers the C conditional operator end to end: the syntax, what actually gets evaluated, the type rules that C really has (the ones most tutorials copy are C++ rules), the printf trap above, and the MAX macro bug that has shipped in more codebases than anyone would like to admit. Writing C++ rather than C? The companion guide to the ternary operator in C++ covers nested ladders, constexpr use and the lvalue difference from the C++ side. Every program on this page was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 using -std=c11 -Wall -Wextra, and the cross-language comparison was re-run with g++ 13.3 under -std=c++17. Every output block, compiler warning and assembly listing is captured verbatim.

What Is the Ternary Operator in C?

The ternary operator (?:), properly called the conditional operator, is C’s only three-operand operator. It takes the form condition ? value_if_true : value_if_false. The condition is evaluated first; if it is non-zero the whole expression takes the value of the second operand, otherwise the third. Only the chosen operand is evaluated, and the result is a value — not an lvalue, and never a statement.

The condition must be of scalar type — any arithmetic type or any pointer type. That includes double, which matters because the previous version of this page claimed the first operand “must be of integer or pointer type.” It need not be. The test is only whether the value compares unequal to zero — which, incidentally, makes NaN a true condition, since NaN compares unequal to everything including zero:

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;

    double d = 3.14;
    printf("double condition:  %d\n", d ? a : b);
    printf("zero double:       %d\n", 0.0 ? a : b);

    int *p = &a;
    printf("pointer condition: %d\n", p ? a : b);

    return 0;
}

Output:

double condition:  10
zero double:       20
pointer condition: 10

Compiled with -Wall -Wextra -Wpedantic, zero warnings. A double condition is perfectly legal C; it is compared against zero like everything else.

The Basic Example, Done Properly

The canonical use is picking the larger of two values:

#include <stdio.h>

int main(void)
{
    int a, b;

    printf("Enter two integers: ");
    if (scanf("%d %d", &a, &b) != 2) {
        fprintf(stderr, "Expected two integers.\n");
        return 1;
    }

    int max = (a > b) ? a : b;
    printf("%d is the larger of %d and %d\n", max, a, b);

    return 0;
}

Output:

Enter two integers: 45 33
45 is the larger of 45 and 33

The if (scanf(...) != 2) check is the part worth copying. scanf returns how many items it successfully converted; ignoring that return value means that when a user types a letter, a and b are read uninitialized and the program’s behaviour is undefined. The version previously on this page ignored it.

The reason to reach for ?: here rather than if-else is not brevity. It is that ?: produces a value, so it can initialize max in the same statement that declares it. An if-else cannot — it forces you to declare max uninitialized first and assign to it afterwards.

Only One Branch Is Evaluated

This is the property the old version of this page never mentioned, and it is the one that matters most in practice. There is a sequence point after the condition, and exactly one of the other two operands is evaluated:

#include <stdio.h>

static int calls = 0;

static int track(int value)
{
    calls++;
    printf("  evaluated %d\n", value);
    return value;
}

int main(void)
{
    int a = 10, b = 20;

    printf("ternary:\n");
    int chosen = (a > b) ? track(a) : track(b);
    printf("  result %d, track() called %d time(s)\n", chosen, calls);

    return 0;
}

Output:

ternary:
  evaluated 20
  result 20, track() called 1 time(s)

track(a) never ran. If the untaken branch had allocated memory, written a file or incremented a counter, none of it would have happened. That is a feature — it is what makes ptr ? ptr->field : fallback safe — but it means a ternary is a poor place to put anything you need to happen.

The Type Trap: Where printf Goes Wrong

Here is the part that separates the C ternary from the C++ one, and the reason this section exists.

Both branches must resolve to a single common type, and that resolution happens at compile time, before anyone knows which branch will run. The usual arithmetic conversions apply, exactly as they would for + or *.

The value comes from one branch. The type comes from both. int flag = 1; flag ? 1 : 2.0 1 int literal — branch taken this is the branch that is evaluated 2.0 double — never evaluated but its type still counts common type of the expression: double sizeof is 8, not 4 — decided at compile time Which is why the format specifier has to match the type, not the branch printf("%d", flag ? 1 : 2.0); undefined behaviour — printed 117454184 printf("%f", flag ? 1 : 2.0); correct — printed 1.000000 Captured from gcc 13.3, -std=c11 -Wall -Wextra. The compiler does warn: -Wformat= reports int vs double. C++ has the same type rule, but streams deduce the type themselves, so the printf hazard is C-specific.
The value comes from one branch, the type from both: in flag ? 1 : 2.0 with flag true, only the int branch is evaluated, but the expression's type is double because both operands determine it at compile time. That is why printf needs %f here and not %d — captured from GCC 13.3 with -std=c11 -Wall -Wextra.
#include <stdio.h>

int main(void)
{
    int flag = 1;

    printf("value    : %f\n", flag ? 1 : 2.0);
    printf("size     : %zu bytes\n", sizeof(flag ? 1 : 2.0));

    return 0;
}

Output:

value    : 1.000000
size     : 8 bytes

flag is true, so the branch taken is the integer literal 1 — yet the result is 1.000000 and occupies 8 bytes. The int was converted to double, because the other branch is a double and the expression has one type regardless of which way it goes.

Now print the same expression with %d:

printf("%d\n", flag ? 1 : 2.0);

GCC catches it, and the warning is worth reading closely:

warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
    5 |     printf("%d\n", flag ? 1 : 2.0);
      |             ~^     ~~~~~~~~~~~~~~
      |              |              |
      |              int            double
      |             %f

Compiled anyway, it printed 117454184 on this machine — a meaningless number, and undefined behaviour, so a different build could print anything at all.

This hazard is specific to C. In C++ the same type rule applies, but std::cout deduces the type from the argument itself, so there is no format specifier to get wrong. It is one of the few places where C’s ternary behaves in a way that a C++ tutorial will not warn you about.

The practical rule: if the branches have different types, either make them match or write the format specifier for the common type, not for the branch you expect to be taken.

The MAX Macro Trap

Every C codebase eventually grows this macro, and it is built entirely from a ternary:

#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main(void)
{
    int i = 5, j = 3;

    int m = MAX(i++, j++);

    printf("m = %d   i = %d   j = %d\n", m, i, j);
    return 0;
}

Output:

m = 5   i = 7   j = 4

Wait — i went from 5 to 7. It was incremented twice, and m is wrong. Ask the preprocessor why:

int m = ((i++) > (j++) ? (i++) : (j++));

i++ appears twice, so it runs twice. The macro looks like a function call and is not one; the argument is pasted in textually at every occurrence. This compiles with zero warnings under -Wall -Wextra.

The fix is not a cleverer macro. It is to stop using one:

static inline int max_int(int a, int b) { return a > b ? a : b; }

Output:

m = 5   i = 6   j = 4

Correct value, each argument evaluated exactly once, and the compiler inlines it so there is no call overhead. The ternary is still doing the work — it just lives somewhere its operands are evaluated once. For multiple types, C11’s _Generic gives you a type-generic wrapper without reintroducing the macro’s double-evaluation problem.

The Everyday Idiom Worth Knowing

The most common good use of ?: in real C is not finding maxima. It is selecting a string literal inside a printf:

#include <stdio.h>

int main(void)
{
    for (int n = 0; n <= 2; n++)
        printf("%d file%s found\n", n, n == 1 ? "" : "s");

    return 0;
}

Output:

0 files found
1 file found
2 files found

Both branches are char *, so the common type is char * and there is no conversion trap. This is the shape ?: is best at: a small value chosen inline, where an if-else would mean duplicating the whole printf.

Nested Ternaries

Nesting is legal and the operator is right-associative, so a ? b : c ? d : e groups as a ? b : (c ? d : e). The three-value comparison the old version of this page used still works:

max = (a > b) ? (a > c ? a : c) : (b > c ? b : c);

With a = 4, b = 5, c = 6 this yields 6. Read it as: if a beats b, the answer is the better of a and c; otherwise it is the better of b and c. The old explanation of this line on this page was wrong in both halves — it said that when a is smaller than c the expression takes “the value of a,” and made the same slip for b. In both cases the answer is c.

As a readability rule rather than a language one, one level of parenthesized nesting is where I stop in C. The C++ companion covers the vertical decision ladder formatting for longer chains, and that formatting works identically in C.

Ternary vs if-else, and the Performance Claim

The previous version of this page said the ternary’s advantage is to “reduce the number of lines of code and improve the performance of application.” The first half is true and minor. The second half is false.

Compiling return a > b ? a : b; and the equivalent if-else with gcc -O2:

ternary                 if-else
-------                 -------
endbr64                 endbr64
cmpl    %edi, %esi      cmpl    %esi, %edi
movl    %edi, %eax      movl    %esi, %eax
cmovge  %esi, %eax      cmovge  %edi, %eax
ret                     ret

Five lines each, one of which — endbr64 — is control-flow-enforcement instrumentation rather than part of the comparison. The four that do the work are the same instructions, in the same order, both branchless — cmovge is a conditional move, so neither version contains a jump at all. They differ only in which operand order GCC chose for the comparison, which has no performance consequence whatsoever. That is one compiler at one optimisation level: it disproves the claim that the ternary is inherently faster, which is all a single counterexample needs to do, but it is not a guarantee that every compiler emits identical code for the two forms.

One honest detail: these are not byte-identical in C, and I expected them to be. Compiled as C++ with g++ -O2, the same two functions produced identical listings; GCC’s C front end happened to canonicalise the comparison the other way round. The C++ companion article reports byte-for-byte identity, and that holds for C++ — it does not quite hold for C, though the difference is cosmetic.

AspectTernary ?:if-else
KindExpression — produces a valueStatement — performs actions
Best forChoosing between two valuesChoosing between two actions
Initializing a constWorksImpossible
Multiple statements per branchNoNatural
Performance at -O2Same instructions, branchlessSame instructions, branchless

Choose on whether you are selecting a value or performing an action. Never on speed. If you are still learning the statement form, our guide to conditional statements in C starts from the beginning.

The Type Rules C Actually Has

The rule list on the old version of this page was borrowed from C++ and does not describe C. It referred to “reference types” and to operands with “user-defined type” — C has neither. References are a C++ feature, and C has no user-defined conversions.

Here is what C actually specifies for the second and third operands:

Both operands are…Result type
The same typeThat type
Arithmetic typesThe usual arithmetic conversions produce a common type
Both voidvoid
The same structure or union typeThat structure or union type
Compatible pointer typesA pointer to the composite type
One a pointer, the other a null pointer constantThe pointer type
One a pointer to void, the other an object pointerPointer to void

Two further points the old list missed entirely. ?: has very low precedence — lower than almost everything, so wrap it in parentheses inside a larger expression; the cppreference C operator precedence table shows exactly where it sits. And the result of a conditional expression in C is never an lvalue, so (a > b ? a : b) = 0; is a compile error:

error: lvalue required as left operand of assignment

MSVC agrees, and says it more plainly — compiling the same file as C with /TC gives error C2106: '=': left operand must be l-value, while /TP compiles it as C++ without complaint. Same file, same compiler, two languages, two answers.

That same line compiles in C++, where the result can be an lvalue. It is the clearest behavioural difference between the two languages’ versions of this operator, and the C++ companion article demonstrates it from the other side.

Key Takeaways

  • condition ? if_true : if_false — the condition may be any scalar type, including double and pointers, not just integers.
  • Only the chosen branch is evaluated. Side effects in the untaken branch never happen, which is what makes ptr ? ptr->x : fallback safe from dereferencing a null pointer, and makes a ternary a bad place for work you need done.
  • The type is decided by both branches at compile time, so flag ? 1 : 2.0 is a double even when the int branch runs — and printf("%d", ...) on it is undefined behaviour.
  • #define MAX(a,b) ((a)>(b)?(a):(b)) evaluates one argument twice. MAX(i++, j++) incremented i from 5 to 7. Use a static inline function instead.
  • The ternary is not faster than if-else. At -O2 both compiled to the same five branchless instructions here. Choose on value-versus-action, never speed.
  • In C the result is never an lvalue — (a > b ? a : b) = 0; is an error in C and legal in C++.
  • Wrap ternaries in parentheses inside larger expressions; ?: binds more loosely than nearly everything around it.

Frequently Asked Questions

Conclusion

The conditional operator has a narrow job: turn a two-way decision into a value. Nearly every problem people have with it comes from forgetting one half of that description — either treating it as a compressed if-else and cramming statements into it, or forgetting that “a value” means a value of one fixed type chosen before the program runs.

Get those two ideas straight and the traps on this page stop being traps. The printf bug becomes obvious, the MAX macro becomes something you write as a function instead, and the performance question stops being a question. The rest of our C programming guides work the same way: the rule, the code that proves it, and the failure it prevents.

What I Could Not Verify

Everything here was compiled and run on one machine: an Ubuntu 24.04 VM with GCC 13.3 and g++ 13.3. GCC, Clang and MSVC were tested, so the assembly listings are GCC’s choices at -O2 and another compiler may canonicalise differently — the claim that the two forms are equally fast rests on them being the same instruction sequence, which is compiler-specific evidence for a language-level point. The garbage value 117454184 from the %d trap is undefined behaviour and will differ between runs, builds and machines; it is shown to demonstrate that the result is meaningless, not that it is predictable. The C type-conversion table is the C standard’s rules stated in summary form; I could not verify a current cppreference URL for the C conditional-operator reference page this session, so that reference is described rather than linked.

Scroll to Top