Chapter 18: Type-generic Programming
Level 3 -- Experience
_Generic, type inference, and anonymous functions -- writing code that works across types in C, and how Auto's generics system makes it natural.
C was designed as a typed language: every object has a type, and operations are defined for specific types. But real programs need to operate on data regardless of its exact type. C has evolved mechanisms for type-generic programming -- _Generic, auto, typeof -- and Auto takes these ideas further with a full generics system.
18.1 Inherent Type-generic Features
C has always had implicit type-generic features through conversions and promotions:
// C Deep Dive: implicit conversions
short s = 42;
int i = s; // short promotes to int (integer promotion)
long l = i; // int converts to long (usual conversion)
double d = l; // long converts to double (usual conversion)
int a = 3;
double b = 4.2;
double c = a + b; // a is converted to double before additionThe usual arithmetic conversion rules determine the common type for binary operations:
- Integer promotions are performed on both operands.
- If the types differ, the "narrower" type is converted to the "wider" type.
- If both are integer types, the signedness is resolved according to complex rules in the standard.
C Deep Dive: The integer promotion rules in C are a frequent source of surprises. On most platforms,
shortvalues are promoted tointbefore any arithmetic operation. This meansshort a, b; auto c = a + b;produces anint, not ashort. The rules also interact with signedness in non-obvious ways:unsigned intvslongdepends on platform sizes.
These conversions are automatic but not always safe:
// C Deep Dive: conversion pitfalls
int big = 2147483647; // INT_MAX on 32-bit int
float f = big; // may lose precision (float has 23-bit mantissa)
int back = f; // implementation-defined if f > INT_MAX
unsigned int u = -1; // well-defined: UINT_MAX via modular conversion
int signed_val = u; // implementation-defined if u > INT_MAXAuto's approach. Auto requires explicit conversions between types. No implicit narrowing conversions:
// Auto: explicit conversions
fn main() {
let i int = 42
let d float = float(i) // explicit conversion
let s int = int(d) // explicit conversion
print("i:", i)
print("d:", d)
print("s:", s)
}18.2 Generic Selection
C11 introduced _Generic for compile-time type dispatch:
// C Deep Dive: _Generic
#include <stdio.h>
#define print_val(x) _Generic((x), \
int: print_int, \
double: print_double, \
char*: print_string \
)(x)
void print_int(int x) { printf("%d\n", x); }
void print_double(double x) { printf("%f\n", x); }
void print_string(char *x) { printf("%s\n", x); }
int main(void) {
print_val(42); // calls print_int
print_val(3.14); // calls print_double
print_val("hello"); // calls print_string
return 0;
}_Generic is a compile-time construct. The compiler selects the matching association based on the type of the controlling expression. It does not evaluate the expression at runtime.
C Deep Dive:
_Genericis powerful but verbose. Each type needs its own function, and the mapping must be maintained manually. It also has surprising behavior with qualified types:const intandintare different types for_Generic, so you may need entries for both.
The <tgmath.h> header uses _Generic to provide type-generic math functions:
// C Deep Dive: tgmath.h
#include <tgmath.h>
double d = sqrt(2.0); // calls sqrt(double)
float f = sqrtf(2.0f); // calls sqrtf(float) -- but with tgmath:
float g = sqrt(2.0f); // also calls sqrtf! type-generic dispatchAuto's approach. Auto plans to provide generics through spec constraints:
// Auto: generics with spec constraints (planned)
// fn add<T>(a T, b T) T where T: Numeric {
// a + b
// }
// For now, use separate functions or comptime dispatch
fn generic_add(a int, b int) int {
a + b
}
fn generic_add_f(a float, b float) float {
a + b
}