Polyglot
Languages C pattern matching
C § pattern-matching

Pattern matching

C does not have pattern matching. The closest mechanism the language offers is switch, which compares an integer expression against integer-constant labels and transfers control to the matching case. Where languages with pattern matching can destructure values, bind sub-components, and discriminate on shape, C’s switch discriminates only on numeric equality and produces no bindings. This page documents what switch is, the idioms it supports, and the patterns C programmers conventionally use to approximate richer matching when it is needed.

switch as integer dispatch

The controlling expression of a switch must have integer type (after the integer promotions); each case label is an integer constant expression. The semantics is a transfer to the matching label, with implicit fallthrough thereafter unless break is reached:

switch (token.kind) {
    case TOKEN_PLUS:    handle_plus();   break;
    case TOKEN_MINUS:   handle_minus();  break;
    case TOKEN_NUMBER:  handle_number(); break;
    default:            handle_other();  break;
}

The construct is structurally equivalent to a series of if/else if comparisons against integer constants, but the compiler is permitted (and expected) to compile it more efficiently: small dense ranges of case labels become jump tables; sparse ranges become binary searches. For the same conditional structure expressed as if/else if, the optimiser typically produces the linear comparison cascade.

The constraints on switch:

  • The discriminant must have integer type. Strings, floating-point, and structures cannot be switched on directly.
  • Case labels must be integer constants. A variable, a function call, or any non-constant expression is not permitted.
  • Two case labels in the same switch may not have the same value; the compiler diagnoses.
  • Fallthrough between cases is implicit; break must be written explicitly to terminate a case.

The first restriction — integer-only discrimination — is the principal limitation that distinguishes C’s switch from richer matching constructs.

Switching on enumerated types

switch is at its most useful when the discriminant is an enumerated type and every enumerator has its own case:

typedef enum { TOKEN_PLUS, TOKEN_MINUS, TOKEN_NUMBER, TOKEN_NAME } token_kind;

const char *token_kind_name(token_kind k) {
    switch (k) {
        case TOKEN_PLUS:   return "+";
        case TOKEN_MINUS:  return "-";
        case TOKEN_NUMBER: return "number";
        case TOKEN_NAME:   return "name";
    }
    return "unknown";
}

Omitting default and listing every enumerator is a useful discipline: with -Wswitch enabled (the default in most configurations), the compiler diagnoses any newly added enumerator that the switch does not handle. This is the closest C comes to exhaustiveness checking.

If the switch contains a default clause, the compiler does not warn about unhandled enumerators, since the default is presumed to handle them. The trade-off — less safety, more flexibility — depends on whether the enumerator set is expected to grow.

Range labels (extension)

The standard does not permit ranges as case labels, but GCC and Clang support the syntax case N ... M: as an extension:

switch (c) {
    case '0' ... '9':  handle_digit(c); break;     /* GCC/Clang extension */
    case 'a' ... 'z':  handle_letter(c); break;
    case 'A' ... 'Z':  handle_letter(c); break;
}

The construction is convenient for character classification but is non-portable; portable code uses <ctype.h> (isdigit, isalpha) or explicit comparisons.

Stacked labels

Multiple case labels with no body between them all transfer to the same statement:

switch (c) {
    case 'a':
    case 'A':
        handle_a();
        break;
    case 'e':
    case 'E':
        handle_e();
        break;
}

The pattern is the conventional way to handle equivalence classes within a switch — case-insensitive characters, multiple opcodes that share an implementation. It is one of the few uses of fallthrough that does not provoke a warning, since the empty body between labels is unambiguous.

Tagged unions

The closest C analogue to algebraic data types is the tagged union: a structure containing a discriminator field and a union of payloads, one per discriminator value:

typedef enum { SHAPE_CIRCLE, SHAPE_SQUARE, SHAPE_TRIANGLE } shape_kind;

typedef struct {
    shape_kind kind;
    union {
        struct { double radius; } circle;
        struct { double side;   } square;
        struct { double a, b, c; } triangle;
    };
} shape;

double shape_area(const shape *s) {
    switch (s->kind) {
        case SHAPE_CIRCLE:
            return 3.14159 * s->circle.radius * s->circle.radius;
        case SHAPE_SQUARE:
            return s->square.side * s->square.side;
        case SHAPE_TRIANGLE: {
            double a = s->triangle.a, b = s->triangle.b, c = s->triangle.c;
            double p = (a + b + c) / 2;
            return sqrt(p * (p - a) * (p - b) * (p - c));
        }
    }
    return 0.0;
}

The combination — enum discriminator, union payload, anonymous unions to flatten the syntax (since C11), switch to dispatch — gives the surface of pattern matching:

  • The discriminator and the payload are tied together by convention; it is the programmer’s responsibility to read only the active variant.
  • The dispatch is via switch, which gets exhaustiveness checking via -Wswitch.
  • The destructuring is manual: s->circle.radius extracts the field of the active variant.

The mechanism is the conventional way to express “a value is one of several alternatives” in C. The discipline is the same as in any tagged-union encoding: never read a variant that is not active, change the discriminator and the payload as a single operation, and prefer functions that consume the union (returning the answer) over code that reads variants from many places.

Anonymous unions

Before C11, the union member would have to be named:

typedef struct {
    shape_kind kind;
    union {
        struct { double radius; } circle;
        struct { double side; } square;
    } u;
} shape;

/* access: s->u.circle.radius */

C11 permits the union to be anonymous, flattening the access path:

typedef struct {
    shape_kind kind;
    union {
        struct { double radius; } circle;
        struct { double side; } square;
    };
} shape;

/* access: s->circle.radius */

The anonymous form is conventional in modern C. The substantive difference is purely syntactic; the storage layout is identical.

_Generic

C11 introduced _Generic, a type-based selection construct that operates at compile time:

#define ABS(x) _Generic((x),         \
    int:    abs,                     \
    long:   labs,                    \
    double: fabs,                    \
    default: abs                     \
)(x)

int     a = ABS(-5);
double  d = ABS(-3.14);

_Generic selects one of the listed expressions based on the type of its first operand and evaluates to that expression’s value. The operand is not actually evaluated; only its type is examined.

The construct is type-driven, not value-driven, so it is not a substitute for tagged unions: it cannot examine a runtime value, only a compile-time type. Its principal use is type-generic macros — <tgmath.h>’s math overloading is a paradigmatic case. As a substitute for value-pattern-matching it offers nothing.

Equivalents to common matching patterns

Languages with pattern matching support several constructions that C must approximate by other means.

Destructuring

/* in a hypothetical pattern-matching language:
   match expr {
       Add(left, right) => left + right
       Mul(left, right) => left * right
   }
*/

/* in C: */
double evaluate(const expr *e) {
    switch (e->kind) {
        case EXPR_ADD: return evaluate(e->add.left) + evaluate(e->add.right);
        case EXPR_MUL: return evaluate(e->mul.left) * evaluate(e->mul.right);
    }
    return 0.0;
}

The destructuring is manual: the function reads the relevant fields directly. The discipline is to access only the variant indicated by the discriminator.

Guards

/* with guards in a pattern-matching language:
   match n {
       0       => "zero"
       n if n < 0 => "negative"
       n       => "positive"
   }
*/

/* in C: */
const char *classify(int n) {
    if (n == 0) return "zero";
    if (n < 0)  return "negative";
    return "positive";
}

switch cannot express guards; the comparison must be either numeric equality (a case) or a richer condition (an if/else if). Mixing the two is not supported.

Or-patterns

/* in a pattern-matching language:
   match c {
       'a' | 'e' | 'i' | 'o' | 'u' => true
       _                           => false
   }
*/

/* in C: */
bool is_vowel(int c) {
    switch (c) {
        case 'a': case 'e': case 'i': case 'o': case 'u':
            return true;
        default:
            return false;
    }
}

Stacked case labels are C’s mechanism for or-patterns. The form is more verbose than the dedicated syntax but expresses the same logic.

Limits of switch-based dispatch

The switch mechanism is adequate for the discriminations C programs typically need: state-machine transitions, tagged-union dispatch, opcode handling. Its limits — single-axis discrimination, integer-only labels, no destructuring, no guards, no exhaustiveness for non-enumerated types — are usually accommodated by structuring the code so that complex matching becomes a sequence of simpler discriminations, or by using if/else if chains where guards are necessary. For genuinely complex matching, a code generator (lex/yacc, ragel, hand-written tables) is the conventional approach; the matching logic is encoded in the generated tables, and the C source contains the dispatcher and the action handlers.