Menu Share

Writing generic code in C – Part 2

by | Published on
category C / C++

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)

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.