Search code examples
cgenerics

Why do C generics use void * instead of macros?


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?


Solution

  • 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.