Martin Uecker, 2025-09-21
I discuss the implementation of type and bounds safe generic containers in C.
Previously, I discussed a span type,
bounds checking using arrays, a
a vector type, a maybe
type.
This time, I will discuss variadic
type. This type
can used to store a value of an arbitrary type, while maintaining
type safety at run-time.
variadic *vp = variadic_make(int, 5);
variadic_access(int, vp)++;
Again, we create this type using a generic macro that expands into a structure that contains a typeid as a string and the generic value.
typedef struct variadic_id { const char *typeid; } variadic;
#define variadic(T) struct val_##T { variadic super; T value; }
But this time, this is not the type we want to use in the code. Instead,
we want to operate on the type variadic
where the type of the
value was erased. We achieve this simply by using a pointer to the first
element of the structure, which contains the typeid and was wrapped into
a structure type variadic
for exactly this purpose.
For convenience, we directly construct pointers to this type using
a macro variadic_make
where internally we create a string
as the typeid for the type argument T
.
#define _variadic_id(T) STRINGIFY(CONCAT(val_, T))
#define variadic_make(T, x) &((variadic(T)){ { _variadic_id(T) }, (x) }.super)
We can now pass around pointers to this type to generic functions.
void generic_algo(variadic *m);
static int algo(int x)
{
variadic *m = variadic_make(int, x);
generic_algo(m);
return variadic_access(int, m);
}
Later, we access the stored value using variadic_access
. How does
this work? We check the typeid at runtime using a string comparison and then
use the fameous containerof
macro to convert the pointer to the
first argument to a full structure of type variadic(T)
. We can
then simply return the stored value.
#define variadic_access(T, x) \
(*({ \
auto _x = (x); \
CHECK(0 == strcmp(_x->typeid, _variadic_id(T))); \
&containerof(_x, variadic(T), super)->value; \
}))
Instead of handling the error condition with the CHECK
macro,
I could create an lvalue that points nowhere in case of an error because it
then corresponds to (*({ (void*)0; }))
, relying on the null
sanitizer or the operating system to transform it into a run-time trap for safety.
This is what I did for maybe
, but one can also add an explicit
check as shown here.
One could ask how the generic function can use the value, as it is now hidden
inside the type-erased variadic
type. The answer is that it calls
other generic functions, which may be passed using a table of function pointers.
typedef struct genops {
void (*increment)(variadic *);
bool (*test)(variadic *);
void (*dbl)(variadic *, variadic *);
} genops;
static void generic_algo(genops vt, variadic *val)
{
vt.dbl(val, val);
if (vt.test(val))
vt.increment(val);
}
You can find the full example here: Godbolt
If you compile this program using a high optimization level, the compiler will
completely remove all overhead and checking and directly return the result.
"main":
mov eax, 35
ret
If you want, you can check out my experimental library where I am experimenting with these ideas: link. If you have ideas on how to do this better, let me know!