Function Overloading in C Using _Generic Macros

Traditional C does not natively support function overloading, requiring developers to write distinct function names for operations that perform the same logic on different data types. However, starting with the C11 standard, the _Generic keyword enables compile-time type association. By wrapping _Generic expressions inside preprocessor macros, developers can map a single function call identifier to type-specific implementations, achieving true compile-time function overloading with zero runtime overhead.

The Mechanics of _Generic

The _Generic selection expression evaluates the type of a controlling expression at compile time and resolves to a specific replacement expression based on an association list. Its general syntax is:

_Generic(controlling_expression, \
    type1: result1, \
    type2: result2, \
    default: default_result \
)

Because the evaluation occurs entirely during compilation, the controlling expression is not evaluated at runtime, meaning expressions with side effects (such as i++) will not execute.

Implementing Single-Argument Function Overloading

To overload a function for multiple types, define the distinct, type-specific functions first. Then, define a macro that uses _Generic on the incoming argument to select and invoke the corresponding function.

#include <stdio.h>

// Type-specific implementations
void print_int(int val) {
    printf("Integer: %d\n", val);
}

void print_double(double val) {
    printf("Double: %f\n", val);
}

void print_string(const char *val) {
    printf("String: %s\n", val);
}

// Overloaded macro
#define print(X) _Generic((X), \
    int: print_int, \
    double: print_double, \
    char*: print_string, \
    const char*: print_string \
)(X)

int main(void) {
    print(42);               // Resolves to print_int(42)
    print(3.14159);          // Resolves to print_double(3.14159)
    print("Hello, World!");  // Resolves to print_string("Hello, World!")
    return 0;
}

In this implementation, the _Generic expression resolves directly to the function pointer matching the type of X. The outer (X) then executes the selected function with the provided argument.

Handling Multiple Arguments

While _Generic inspects only one controlling expression per statement, you can achieve multi-argument overloading by nesting _Generic expressions.

#include <stdio.h>

void add_int_int(int a, int b) { printf("int + int = %d\n", a + b); }
void add_double_double(double a, double b) { printf("double + double = %f\n", a + b); }
void add_int_double(int a, double b) { printf("int + double = %f\n", a + b); }

#define add(A, B) _Generic((A), \
    int: _Generic((B), \
        int: add_int_int, \
        double: add_int_double \
    ), \
    double: _Generic((B), \
        double: add_double_double \
    ) \
)(A, B)

Key Considerations and Best Practices

  • Default Associations: Always consider providing a default branch to prevent cryptic compiler errors or to invoke a safe fallback implementation.
  • Lvalue Decay: Arguments passed into _Generic undergo standard type conversions: array names decay into pointers, and functions decay into function pointers. Qualifiers like const on non-pointer values are dropped during type selection.
  • Strict Type Safety: All branches in a _Generic expression must be syntactically valid code, even if they are not selected. If you resolve to expressions rather than function identifiers, ensure that every branch compiles against the argument context.
  • Performance: Because dispatch happens during the compilation phase, _Generic introduces no dynamic dispatch tables, branching instructions, or cache misses. The generated assembly is identical to calling the underlying specialized functions directly.