As I understand it, the standard way to implement generic data types in C is with void pointers. However, an alternate approach would be to use macros. Here's an example implementation of a generic "Option" type using macros:
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#define OPTION(T) \
struct option_##T { \
bool exists; \
T data; \
}; \
typedef struct option_##T option_##T; \
static inline bool is_some_##T(option_##T x) { return x.exists; } \
static inline bool is_none_##T(option_##T x) { return !x.exists; }
#define is_some(x) _Generic((x), option_int: is_some_int, option_char: is_some_char)(x)
#define is_none(x) _Generic((x), option_int: is_none_int, option_char: is_none_char)(x)
OPTION(int);
OPTION(char);
int main(int argc, char *argv[]) {
option_int x = {.exists = true, .data=3};
option_char y = {.exists = true, .data='a'};
printf("x is some: %d\n", is_some(x));
printf("y is some: %d\n", is_some(y));
return 0;
}
The alternate approach would be to use a void * for the data entry. However, this would add an extra layer of indirection (while possibly saving on binary size). Is there a reason the void * approach is more common?
Because _Generic
is a relatively new feature. The "old school" way to do it was always with void pointers, either by creating a struct similar to your example but typically with an enum to mark the type represented. Or otherwise by type-specific callbacks as famously done by bsearch
/qsort
.
So void*
is more common simply for historical reasons - there was no other way to do it back in the days. void*
are becoming increasingly irrelevant these days, because they are dangerously type unsafe and there are better language features available now.
As for _Generic
and soon-to-be-standardized typeof
, there's a lot of different ways to use them. There's not really a need for wrapper structs/enums either. For example you can use them as "poor man's templates" - perhaps not recommended practice but quite powerful and type safe:
#include <stdio.h>
#define TYPES_SUPPORTED(X) \
X(int, %d) \
X(char, %c) \
#define PRINT(type, fmt) \
void type##_print (type t) \
{ \
printf(#fmt "\n", t); \
}
TYPES_SUPPORTED(PRINT)
#define GENERIC_PRINT(type, fmt) ,type: type##_print
#define print(x) \
_Generic((x), \
default:0 \
TYPES_SUPPORTED(GENERIC_PRINT) )(x)
int main()
{
int a = 123;
char c = 'A';
print(a);
print(c);
}
This way to utilize "X macros" means that you can show all types supported into a list, no need to call individual macros as in your example. Instead you create one macro per use-case and then call the X macro list.
So this example creates one function int_print(int t)
and one char_print(char t)
, printing the parameter using the correct format specifier.
We can then call these functions in turn from a type-generic print
macro, where the _Generic
association list has been baked into an X macro list too, reducing the need to type out each condition. The evil trick here is to put default:
first and then begin each macro expansion with ,
, since _Generic
unlike array/enum declarations, initialization lists etc does not like trailing comma.