Writing generic code in C – Part 2
After some comments on my previous post about writing generic code in C where people argue that this is “poor man’s overloading” I wanted to add a new technique that allows you to write real generic style code in C with the only drawback. You could even combine the technique from this lesson and the previous one to replicate real generics that exist in other languages.
The problem
One of the great strenghts in C is that it is explicit and it is easy to reason about. The code does what it says and it makes you create explicit functionality for different types, function signatures and so on. This is both a blessing and a curse sometimes since you would have to repeat some sections of code which is not ideal if you later want to modify the general logic of those sections. Imagine having a dynamic array. To make it generally useful for many types and reuse code you would have to do type erasure right? Well in this article I will show you how you can do a dynamic array without type erasing its members. This will have great implications on your later debugging experience of said array and its members.
The main drawback of the approach I am going to show is that you still end up with multiple function names for each different array element type that you want to use but this is something one can easily live with since you’re not writing those functions multiple times and their logic is reused from the same central place.
Prerequisites
Before diving into the generic code I will introduce two macros that will ease up our code generation. Since we’re going to use preprocessor definitions to create more preprocessor definitions we need to have macro expansion functions that will concatenate some names for us:
#define CAT_LITERAL( A, B ) A ## B
#define CAT( A, B ) CAT_LITERAL( A, B )
That way when we want to define some name we can do so by using previous defined values:
#define T int
#define ARRAY_TYPE CAT(T, _array_t)
Graphics Programming with Vulkan
You can get my course on Vulkan which covers the basics of graphics programming through the Vulkan specification. You can get the best price from here:
The technique
So what is the general technique for creating generic code. Well you might’ve already guessed it by the previous section but what you should do is create a new .inc file (by convention, it could just be a header file if you like it). I will create one now and call it array.inc
/// array.inc
/// It is very important to omit any header guards or "#pragma once" from this file.
/// It is meant to be included multiple times
typedef struct CAT(T, _array_t) {
size_t size;
size_t capacity;
T* data;
} CAT(T, _array_t);
#undef T
Okay so a few interesting things happen here. We create a struct called CAT(T, _array_t) which has typical array members. We also use T* data. Isn’t this true generic code? How do we actually use this file?
Well let me answer this straight away. You now create numeric_array.h
/// numeric_array.h
#pragma once
#define T int
#include "array.inc"
#define T char
#include "array.inc"
Since our array.inc file undefines T at the end we can define a new T and reuse the same include file but it will generate new code. And you can do this in any file that you want to reuse an array type from later. Well you might be looking at this and wondering – how do I use unsigned ints though. The concatenation won’t work… Well let me tell you – the magic of the preprocessor is that you can have as many definitions as you want prior to including array.inc. You can even have optional code that you control with definitions since you can use #ifdef . Let’s expand our array:
/// array.inc
#ifndef TName
#define TName T
#endif
#define TArray CAT(TName, _array_t)
typedef struct TArray {
size_t size;
size_t capacity;
T* data;
} TArray;
static TArrayType CAT(CAT(create_, TName), _array)(size_t capacity) {
return (TArray) {
.size = 0,
.capacity = capacity,
.data = malloc(capacity * sizeof(T))
};
}
#undef T
#undef TName
#undef TArray
Now here you can see I added a generic function, a helper definition for the type and we require only T with an optional TName to customize the name of functions.
/// numeric_array.h
#pragma once
#define T int
#include "array.inc"
#define T unsigned int
#define TName uint
#include "array.inc"
#define T char
#include "array.inc"
This would produce the follwing functions then: create_int_array() , create_uint_array() and create_char_array()
Combined with overloading
In my previous post called Writing generic code in C I covered overloading. Well we can now use overloading with our new generated functions to then just create a simple call to create even more templated syntactic sugar.
/// array.inc
/// ...
static void CAT(TName, _array_push)(TArray* array, T value) {
// handle capacity increase if you want
array->data[array->size] = value;
array->size += 1;
}
/// ... undefine section always last
Then in the numeric_array.h you can define a new generic:
/// numeric_array.h
/// ...
#define array_push(arr, value) _Generic(arr, \
int_array_t: int_array_push(arr, value), \
uint_array_t: uint_array_push(arr, value), \
char_array_t: char_array_push(arr, value))
Yes, in this case you do need to know all the overloads you would have but it does produce valid code. And you can still always fallback to the named implementations for cases outside of your predefined generic.
Summary
Generics do exist in C but they do require a bit of file juggling to make them work. Hope this lesson proves to be useful and improves your future code or even helps you redefine your existing codebase where the DRY principle has long been left abandoned and forgotten.
