Skip to content
📖 Auto vs Modern CBook contents

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
// 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 addition

The usual arithmetic conversion rules determine the common type for binary operations:

  1. Integer promotions are performed on both operands.
  2. If the types differ, the "narrower" type is converted to the "wider" type.
  3. 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, short values are promoted to int before any arithmetic operation. This means short a, b; auto c = a + b; produces an int, not a short. The rules also interact with signedness in non-obvious ways: unsigned int vs long depends on platform sizes.

These conversions are automatic but not always safe:

c
// 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_MAX

Auto's approach. Auto requires explicit conversions between types. No implicit narrowing conversions:

auto
// 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
// 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: _Generic is 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 int and int are different types for _Generic, so you may need entries for both.

The &lt;tgmath.h&gt; header uses _Generic to provide type-generic math functions:

c
// 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 dispatch

Auto's approach. Auto plans to provide generics through spec constraints:

auto
// 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
}

Released under the MIT License.