Question

Tricky C Code: Conditional Operator, Comma Operator, and Type Punning

cdeobfuscation

Question

Why does the following C program print C++Sucks on the system where it was tested? What concepts are used in this expression?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main(void) {
    m[1]-- ? m[0] *= 2, main() : printf((char *)m);
}

In particular, how do the conditional operator, comma operator, recursive call, and cast from double * to char * affect the result?

Short Answer

This program combines several legal-but-obscure C features: a conditional expression, the comma operator, recursion, floating-point arithmetic, and reading an array's raw bytes as characters. It prints the shown text only because its numeric values and memory layout were carefully chosen for a particular platform. You will learn how to parse such expressions and why this code should not be used in production.

Concept

The central lesson is operator parsing and evaluation in C, combined with a deliberately nonportable representation trick.

This expression:

m[1]-- ? m[0] *= 2, main() : printf((char *)m);

uses the conditional operator:

condition ? when_true : when_false

The condition is m[1]--. The postfix -- uses the old value first, then subtracts one.

A subtle C grammar rule says that the middle expression of ?: is parsed as though it were parenthesized. Therefore, the expression behaves like this:

m[1]-- ? (m[0] *= 2, main()) : printf((char *)m);

It does not mean this:

// Not the effective grouping
(m[1]-- ? m[0] *= 2 : printf((char *)m)), main();

When m[1] is nonzero, the program doubles m[0] and recursively calls main. When m[1] eventually reaches zero, it instead calls printf.

The unusual numeric constants are selected so that, after repeatedly multiplying the first double by 2, its bytes happen to spell C++Sucks on the tested machine. Casting m to char * makes printf interpret those bytes as a C string.

This output is not portable. It relies on details such as the floating-point format, byte order, how the compiler converts the decimal literal to double, and the resulting bytes containing a null terminator.

Mental Model

Think of the conditional operator as a fork in a path:

  • If the countdown has not reached zero, take the repeat path.
  • If it has reached zero, take the print path.

The comma operator is like completing two chores in order on the repeat path:

  1. Double m[0].
  2. Call main() again.

The double array is normally a pair of number containers. The cast to char * tells printf: “Do not treat this memory as numbers; inspect the same storage one byte at a time as text.”

That last step is like looking at the labels hidden in the raw packaging of a product rather than using the product normally. It can reveal a meaningful message only because someone intentionally arranged the bytes that way.

Take Quiz

Syntax and Examples

The conditional operator chooses one of two expressions:

result = condition ? value_if_true : value_if_false;

The comma operator evaluates its left expression, discards that expression's value, then evaluates and produces the value of its right expression:

first_action(), second_action();

Example:

#include <stdio.h>

int main(void) {
    int count = 2;

    count-- ? (puts("Count was nonzero"), puts("Repeating"))
            : puts("Count was zero");

    return 0;
}

count-- evaluates to 2, then changes count to 1. Since 2 is true in C, the true branch runs. The comma operator ensures that both puts calls run from left to right.

A clearer equivalent is usually better:

if (count-- != 0) {
    puts("Count was nonzero");
    puts("Repeating");
} else {
    puts("Count was zero");
}

In C, zero is false and every nonzero numeric value is true.

Step by Step Execution

Consider a smaller, readable version of the control flow:

#include <stdio.h>

int count = 2;

void repeat(void) {
    count-- ? (puts("work"), repeat()) : puts("done");
}

int main(void) {
    repeat();
    return 0;
}

Execution trace:

  1. count starts as 2.
  2. count-- evaluates to 2, then count becomes 1.
  3. 2 is nonzero, so the true branch runs: it prints work and calls repeat() again.
  4. count-- now evaluates to 1, then count becomes 0.
  5. 1 is nonzero, so it again prints work and recurses.
  6. count-- now evaluates to 0, then count becomes -1.
  7. 0 is false, so the false branch runs and prints done.

In the original program, the same process occurs 771 times. Each true branch performs:

m[0] *= 2;
main();

At the final call, the false branch executes:

printf((char *)m);

On the intended platform, the bytes beginning at m then represent C++Sucks followed by a zero byte, so printf treats them as its format string.

Real World Use Cases

The exact byte trick is not a real-world pattern, but the individual concepts are common.

  • Conditional operator: Choose a small value or expression, such as a default label.

    const char *status = is_ready ? "ready" : "waiting";
    
  • Postfix decrement: Process a fixed number of retries or items.

    while (retries-- > 0) {
        attempt_request();
    }
    
  • Recursion: Traverse tree-shaped data, such as directories, expression trees, or nested configuration data.

  • Byte-level access through unsigned char * or char *: Inspect or copy an object's representation when implementing low-level utilities, serializers, debuggers, or protocol code. This requires careful attention to portability and formats.

  • Null-terminated strings: C library functions such as printf, puts, and strlen expect strings to end with \0.

Take Quiz

Real Codebase Usage

Production C code generally writes this logic explicitly rather than compressing it into one expression.

Prefer a loop for a countdown

for (int remaining = 771; remaining > 0; --remaining) {
    value *= 2;
}

This avoids recursion depth and makes the stopping rule obvious.

Prefer explicit branches for side effects

if (count > 0) {
    --count;
    do_work();
} else {
    report_done();
}

Conditional expressions are best when selecting a value. An if statement is clearer when each branch performs actions.

Treat raw bytes as data, not format strings

If bytes need to be displayed, use an explicit format:

printf("%s\n", text);

For arbitrary binary data, print hexadecimal bytes rather than passing it to printf as a format string:

for (size_t i = 0; i < length; ++i) {
    printf("%02X ", bytes[i]);
}
printf("\n");

In real codebases, validation, clear control flow, bounded loops, and explicit formats are safer than relying on object representations.

Common Mistakes

Misreading the comma operator

Beginners may think the comma only separates parts of the conditional expression. Here it is an operator in the true branch.

// The true branch contains both expressions.
condition ? (do_first(), do_second()) : do_other();

Use parentheses when the grouping is not immediately obvious.

Forgetting that postfix -- uses the old value

int n = 1;
if (n--) {
    /* Runs: the tested value was 1. n is now 0. */
}

If you need to decrement before testing, use --n, but make the intended condition explicit when possible.

Assuming ?: evaluates both branches

Only one branch of the conditional operator is evaluated.

int result = 1 ? 10 : dangerous_function();

dangerous_function() is not called here.

Treating every double as text

This is not a valid way to convert a number to a string:

// Broken idea: these bytes are not normally readable text.
double price = 19.99;
printf("%s\n", (char *)&price);

Use a conversion specification instead:

printf("%.2f\n", price);

Passing untrusted data as printf's format string

// Dangerous if user_text contains format specifiers such as %s or %n.
printf(user_text);

Use:

printf("%s", user_text);

Relying on machine representation

The original output depends on implementation details. A different CPU byte order or floating-point representation can produce different text, no text, or behavior that does not meet the string requirement. Do not store messages inside floating-point bit patterns.

Comparisons

FeatureWhat it doesBest useReadability concern
condition ? a : bEvaluates either a or bSelecting one small value or expressionNested or side-effect-heavy expressions become hard to read
if/elseExecutes one statement blockBranches with several actionsUsually clearer than ?: for side effects
a, b comma operatorEvaluates a, then b; result is bRarely needed in ordinary application codeEasily confused with commas that separate arguments or declarations
Function recursionA function calls itselfNaturally nested structuresCan consume stack space for large counts
LoopRepeats statements iterativelyCounters and repeated workUsually preferred over recursion for simple countdowns
char * view of memoryReads bytes as charactersLow-level byte inspectionDoes not turn arbitrary bytes into a valid C string

The original program uses recursion where a loop would be clearer, and it uses raw bytes as a string where an actual character array would be safer.

Take Quiz

Cheat Sheet

// Conditional operator
condition ? when_true : when_false;

// Comma operator: left first, right second
(first_action(), second_action());

// Postfix decrement: use old value, then decrement
x--;

// Clear equivalent of a side-effect-heavy conditional
if (condition) {
    first_action();
    second_action();
} else {
    fallback_action();
}
  • In C, 0 is false; nonzero values are true.
  • x-- tests or yields the old value, then decreases x.
  • Only the selected ?: branch is evaluated.
  • The middle operand of ?: can contain a comma expression: c ? a, b : d acts like c ? (a, b) : d.
  • printf(format) treats format as a format string. For text data, prefer printf("%s", text).
  • A char * can examine an object's bytes, but object bytes are not automatically a portable or null-terminated string.
  • Prefer loops over recursion for a simple numeric countdown.

FAQ

Why does m[1]-- eventually select the false branch?

m[1]-- evaluates to its current value before decrementing it. Values 771 down to 1 are true; when its current value is 0, the conditional selects the false branch.

Why does m[0] *= 2, main() call main after doubling?

The comma operator evaluates its left operand first (m[0] *= 2), then evaluates its right operand (main()).

Does the comma operator have low precedence in C?

Yes. However, the middle operand of the conditional operator has a special parsing rule, so the comma in this example belongs to the true branch.

Is calling main() recursively valid C?

Calling main from within the program is not a good or portable program design. Even where it works, repeatedly calling it can exhaust the call stack. Put recursive logic in another function instead.

Is casting double * to char * always invalid?

No. C permits inspecting an object's representation through character types. But the observed bytes are implementation-dependent, and they may not form a valid null-terminated text string.

Why is printf((char *)m) risky?

Its first argument is a format string, not merely text. It also assumes the bytes at m form a null-terminated string and contain no unintended % format directives.

Will this print C++Sucks on every compiler?

No. The result depends on platform-specific floating-point representation, endianness, decimal-to-double conversion, and byte layout. It is a demonstration puzzle, not portable C.

Take Quiz

Mini Project

Description

Build a readable countdown program that demonstrates the same control-flow ideas as the puzzle without recursion, hidden byte encodings, or unsafe format-string usage. This is the style appropriate for ordinary application code.

Goal

Print each remaining attempt, then print a completion message after the countdown reaches zero.

Requirements

Create an integer countdown starting at 3. Use a loop to run while attempts remain. Decrement the counter once per iteration. Print the remaining count during each iteration. Print a final message after the loop ends.

Take Quiz

Keep learning

Related questions

View more