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
}