home

C has overloading, actually

December 28nd, 2023
4 minute read

Function overloading is a feature added by C++ which is occasionally very useful. I’m not too sure if it’s worth the extra complexity—name mangling is can get very annoying sometimes—but every now and then overloading is a great feature to have. Today I wanted to show you two tricks to pretty much completely emulate C++ overloading

Type-based overloading

C11 adds the _Generic keyword and type-generic expressions, which may make it seem like C has generics, but not really. It’s more like a type-based switch statement, added to the language exclusively to implement the functions in <tgmath.h>.

Generic expressions look like:

_Generic ( controlling-expression,association-list ).

Where “controlling-expression” is an expression and “association-list” is a list of type to expression pairs. For example:

int foo = 5;
const char *foos_type = _Generic(foo,
	int: "it's an int",
	float: "it's a float",
	default: "idk");

Here the _Generic expression acts like a switch statement on the type of foo and can give us different results based on what type foo is. One limitation is that each type expression has to be semantically valid, which can be regarded as an oversight, but largely doesn’t affect us here.

If, before _Generic, you would write this:

void draw_circle(Circle circle) { ... }
void draw_rectangle(Rectangle rectangle) { ... }
void draw_point(Vector2 point) { ... }

you can now use a macro to switch between function choices.

#define draw(obj) _Generic(obj,  \
	Circle: draw_circle,         \
	Rectangle: draw_rectangle,   \
	Vector2: draw_point          \
)(obj)

int main() {
	Circle c = { .radius = 5.0f; };
	draw(c);

	Rectangle r = { .w = 1, .h = 2 };
	draw(r);

	Vector2 v = { 3, 4 };
	draw(v);
}

Number-of-arguments–based overloading

Okay, that one was easy, we’re just using a C11 feature for entirely it’s intended purpose. Something more useful would be overloading based on the number of arguments passed. C libraries often need to specify somewhere in their name how many arguments they take, or their type (for example, from raylib):

void DrawPixel(int posX, int posY, Color color); // Draw a pixel
void DrawPixelV(Vector2 position, Color color);  // Draw a pixel (Vector version)

It would be nice if we could call both of these with just DrawPixel(...). That way if we ever change to passing a vector, we only need to change one thing. Luckily, with some macro magic, we can. First we’ll name our 2 “overloads” by the number of arguments they take:

void DrawPixel2(Vector2 position, Color color);  // Draw a pixel (Vector version)
void DrawPixel3(int posX, int posY, Color color); // Draw a pixel

Then we can define a macro that always expands to its 4th argument.

#define DrawPixelX(a,b,c,d,...)   d

Finally, this macro expands its arguments into DrawPixelX, followed by the function names DrawPixel3 and DrawPixel2.

#define DrawPixel(...)   DrawPixelX(__VA_ARGS__,DrawPixel3,DrawPixel2)(__VA_ARGS__)

If we give it 2 arguments, it expands like this:

DrawPixel(v, WHITE)
-> DrawPixelX(v, WHITE, DrawPixel3,DrawPixel2)(v, WHITE) // expands to 4th argument
-> DrawPixel2(v, WHTIE)

and for 3,

DrawPixel(1, 2, WHITE)
-> DrawPixelX(1, 2, WHITE, DrawPixel3,DrawPixel2)(1, 2 WHITE) // expands to 4th argument
-> DrawPixel3(1, 2, WHTIE)

The trick is that by having DrawPixelX always expand to its fourth argument, we can “push” the function we want forwards by the amount of arguments we pass in.

Are these macro hacks really a good idea?

I don’t know about real projects, but C macros (especially with C11 and C23) are a lot more powerful than many give credit for. Using the tricks above, plus some other hacks, we can even make a type-safe printf function:

int main() {
    Vec3 v = {1, 2.5, 3};

    // regular printing
    PRINT("%; %; %; %; %\n", 1+2, "Hello world", (void*)0xbeefbabe, v, sinf(M_PI_4));
    // -> "3; Hello world; 0xbeefbabe; {1, 2.5, 3}; 0.707107"

    // missing arguments
    PRINT("% + % = %\n", 1, 2);
    // -> "1 + 2 = %!MISSING"

    // extra arguments
    PRINT("hi", 1+2, "Hello world", (void*)0xbeefbabe, v, sinf(M_PI_4));
    // -> "hi%!(EXTRA int=3, char*=Hello world, void*=0xbeefbabe, Vec3={1, 2.5, 3}, float=0.707107)"

    // unknown type compile error
    struct foo {} f;
    // PRINT("%", f);
    // -> error: controlling expression type 'struct foo' not compatible with any generic association type
}