Generic selection in C11
It's been almost 3 years since my last post. But I'm back!
C11 Introduced _Generic, a keyword that gives us compile-time type dispatch, the closest C gets to function overloading, with zero runtime cost.
Contents
Motivation
C has no function overloading, so for example, printf need to rely on the caller to match format specifiers to types. Get it wrong and you invoke UB (undefined behavior) silently (although modern compilers will warn you):
long size = 100000L;
double ratio = 3.14;
printf("size = %d\n", size); // BUG: %d for a long -> UB
printf("ratio = %d\n", ratio); // BUG: %d for a double -> UB
With _Generic we don't have that issue, and we can build cool stuff like a debug() macro that picks the correct format specifier at compile time, and prints the variable name and its value (for free):
#define fmt(x) _Generic((x), \
int: "%d", \
long: "%ld", \
double: "%f", \
char*: "%s" \
)
#define debug(x) printf(#x ": " fmt(x) "\n", (x))
debug(size); // size = 100000
debug(ratio); // ratio = 3.140000
No wrong format specifiers, no UB, resolved entirely at compile time. Let's see how it works.
What is _Generic?
_Generic is a selection expression that chooses one of several expressions at compile time based on the type of a controlling expression. Think of it as a switch statement, but instead of switching on a value, it switches on a type.
Here's the basic syntax:
_Generic(controlling_expression,
type1: expression1,
type2: expression2,
// ...
default: default_expression
)
The controlling expression is never actually evaluated, only its type is inspected by the compiler. This is important: there are no side effects from the controlling expression, it's purely there for type resolution.
The default association acts as a catch-all, similar to the default case in a switch. If the type of the controlling expression doesn't match any of the listed types, the default expression is selected. The default is optional, but if you omit it and the type doesn't match anything, you'll get a compilation error.
How does it work?
When the compiler encounters a _Generic expression, it follows these steps:
- Determine the type of the controlling expression
- Compare that type against each type in the association list
- Select the expression whose type matches
- Replace the entire
_Genericexpression with the selected expression
The magic here is that all of this happens at compile time. The resulting binary contains only the selected branch, there is no runtime dispatch, no vtable, no function pointers. It's as if you had written the selected expression directly.
A simple example:
#include <stdio.h>
int main(void) {
int x = 42;
const char *type = _Generic(x,
int: "int",
float: "float",
double: "double"
);
printf("x is of type: %s\n", type);
return (0);
}
Output:
x is of type: int
The _Generic expression doesn't have to resolve to values only, it can resolve to function names or any valid expression:
#include <stdio.h>
void print_int(int x) { printf("%d\n", x); }
void print_dbl(double x) { printf("%f\n", x); }
#define print_val(x) _Generic((x), \
int: print_int, \
double: print_dbl \
)(x)
int main(void) {
print_val(42);
print_val(3.14);
return (0);
}
Output:
42
3.140000
Here _Generic resolves to a function name, and we immediately call that function with (x). The compiler sees print_int(42) for the first call and print_dbl(3.14) for the second.
tgmath.h and _Generic
The C standard library already relies on this concept. The <tgmath.h> header provides type-generic math functions that dispatch to the correct variant based on the argument type.
For example, when you call sin() through <tgmath.h>, it dispatches to the right precision:
#include <stdio.h>
#include <tgmath.h>
int main(void) {
float f = 1.0f;
double d = 1.0;
long double l = 1.0L;
// sin() dispatches to sinf(), sin(), or sinl()
// based on the argument type
printf("sinf: %f\n", sin(f)); // calls sinf
printf("sin: %f\n", sin(d)); // calls sin
printf("sinl: %Lf\n", sin(l)); // calls sinl
return (0);
}
Before C11, <tgmath.h> relied on compiler magic to achieve this. With _Generic, there's now a standard, portable mechanism. A simplified version of what <tgmath.h> might look like under the hood:
#include <math.h>
#define sin(x) _Generic((x), \
float: sinf, \
double: sin, \
long double: sinl \
)(x)
Pitfalls
While _Generic is powerful, there are a few gotchas to be aware of.
Integer promotion: char and short values undergo integer promotion in expressions. This means that _Generic may see an int instead of the type you expect:
char c = 'A';
// This might match `int`, not `char`, depending on the compiler
// and whether the controlling expression triggers promotion
const char *t = _Generic(c,
char: "char",
int: "int"
);
In practice, most compilers will match char correctly for a simple variable like c, but be cautious with expressions like c + 0 — that will definitely promote to int.
Array and function decay: Arrays decay to pointers in the controlling expression:
char str[] = "hello";
// str decays to char *, not char[]
const char *t = _Generic(str,
char *: "pointer to char"
);
String literals are char[] (or char[N]), which also decay to char * in this context.
All branches must compile: Even though only one branch is selected, all association expressions must be valid. The compiler type-checks every branch:
int x = 42;
// This will NOT compile, even though the float branch is never selected,
// because int_func must still be a valid expression
_Generic(x,
int: int_func(x),
float: float_func(x) // error if float_func is not declared
);
This is different from if-else or preprocessor #if — you can't use _Generic to conditionally compile code that wouldn't otherwise be valid.