cgenericsmacrosc-preprocessor

How to create meaningful error messages from _Generic macros?


Lets say I have some manner of type safety macro based on _Generic, which I use to wrap an actual function call to make it type safe:

#include <stdio.h>

void print (int i)
{
  printf("%d\n", i);
}

#define print(expr) _Generic((expr), int: print)(expr)

int main()
{
  print(1);
  print(1.0);
}

This makes the code type safe, but I get a very non-descriptive gibberish error message when passing the wrong type:

error: controlling expression type 'double' not compatible with any generic association type

That's not very helpful to the user who might think they were calling a function and not my little type safety wrapper macro. What "generic association" is the compiler even on about, they will ask. Very confusing.

#error will obviously not work here since _Generic is evaluated at compile-time but after pre-processing is done.

How can I display a meaningful, customized compiler error to the caller whenever _Generic fails?


Solution

  • The first step is to move the type check to a separate macro that returns 1 or 0 whether the passed type is supported or not:

    #define type_check(t) _Generic((t), int: 1, default: 0)
    

    And then call static_assert with the macro result as input. So far so good.

    However, in this case we'll also want to execute the actual print function. The normal way to do that in a function-like macro would be to use the comma operator, since it discards the left-most operand. And the result will be that of the right-most operand, which is very nice in case the function also returns something.

    But code like this isn't valid C:

    #define print(expr) ( static_assert(type_check(expr), "error message"), print(expr) )
    

    And that's because static_assert isn't actually an expression but a declaration. So we have to invent a way to turn a declaration into an expression. This is actually quite possible in the following way:

    Example:

    #define static_assert_expr(expr, msg) ( (void)(struct{ int dummy; static_assert(expr, msg); }){}.dummy )
    

    Here the struct is an anonymous declaration with no tag, declaring a struct with a dummy member and a static assert. It returns the dummy member and we cast that to void to silence compiler messages. A void expression on the left side of a comma operator ought to keep the compiler quiet.

    And so the final macro becomes this:

    #define print(expr) ( static_assert_expr(type_check(expr), "Wrong argument " #expr " passed to print, expected int.") \
                          , \
                          print(expr) )
    

    Full example:

    #include <stdio.h>
    
    void print (int i)
    {
      printf("%d\n", i);
    }
    
    #define type_check(t) _Generic((t), int: 1, default: 0)
    
    #define static_assert_expr(expr, msg) ( (void)(struct{ int dummy; static_assert(expr, msg); }){}.dummy )
    
    #define print(expr) ( static_assert_expr(type_check(expr), "Wrong argument " #expr " passed to print, expected int.") \
                          , \
                          print(expr) )
    
    int main()
    {
      print(1);
      print(1.0);
    }
    

    Compiler output:

    error: static assertion failed: Wrong argument 1.0 passed to print, expected int.
       18 |   print(1.0);
          |   ^~~~~~~~~~