DEV Community

_Generic in C

Paul J. Lucas on January 16, 2024

Introduction Among other things, C11 added the _Generic keyword that enables compile-time selection of an expression based on the type o...
Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

For others reading, it’s not an error, but merely a warning. @notgiven chose to add -Werror.

Collapse
 
emincin profile image
Emin Cin • Edited

Hi there,

There is a limit to STATIC_IF: THEN/ELSE does not support statements.

#define STATIC_IF(EXPR,THEN,ELSE)     \
  _Generic( &(char[1 + !!(EXPR)]){0}, \
    char (*)[2]: (THEN),              \
    char (*)[1]: (ELSE)               \
  )

STATIC_IF(true, 1, 0); // OK
STATIC_IF(true, int a = 1, 0); // compile-time error, _Generic doesn't support statements.
Enter fullscreen mode Exit fullscreen mode

Based on your code I created if_constexpr. Just like in C++, it can constrain the if condition to be determined at compile time.

#define comptime_bool(expr) _Generic(&(char[1 + !!(expr)]){ 0 }, \
  char (*)[2]: 1, \
  char (*)[1]: 0)

#define if_constexpr(expr) if (comptime_bool(expr))

  int x = 42;
  if_constexpr (0) {} // OK
  if_constexpr (x) {} // compile-time error: compound literal cannot be of variable-length array type
Enter fullscreen mode Exit fullscreen mode

When compiler optimizations (-O1) are enabled, branches where the if_constexpr (0) will be automatically ignored.

Collapse
 
pauljlucas profile image
Paul J. Lucas

Yes, I'm aware that STATIC_IF has limitations. Perhaps a better name would have been STATIC_IF_EXPR to make it clear that it's intended to be used as an expression and not a statement.

I believe that you don't need comptime_bool at all: just just STATIC_IF as-is where THEN is 1 and ELSE is 0.

Your if_constexpr works, but it's not clear it offers any new functionality since you could just do:

#if 0
// OK
#endif

#if x
// error
#endif
Enter fullscreen mode Exit fullscreen mode

That is, just use the normal #if.

The value of STATIC_IF is precisely that it is an expression, not a statement.

Collapse
 
emincin profile image
Emin Cin • Edited

Ignore comptime_bool it's just for fun :)

#if can't handle something like this:

constexpr int x = 10;
#if (x == 10)
#endif
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Ah, yes, that's true. So if_constexpr builds upon (does not replace) STATIC_IF:

#define if_constexpr(EXPR) \
  if ( STATIC_IF( (EXPR), 1, 0 ) )
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Actually, you can hoist the if out of the macro and make it exactly like if constexpr in C++:

#define constexpr(EXPR) ( STATIC_IF( (EXPR), 1, 0 ) )

// ...
constexpr int X = 0;

if constexpr ( X )
   puts( "yes" );
Enter fullscreen mode Exit fullscreen mode

The reason this works is because the preprocessor will not expand a function-like macro when not followed by (. Hence, defining constexpr(EXPR) as a macro does not “shadow” the real constexpr in C23 that still works. Therefore, you can really write if constexpr with a space between them just like in C++.

Thread Thread
 
emincin profile image
Emin Cin

Woww, this looks really cool. Now I'm trying to write some SFINAE code with if constexpr.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

As I explained in the article, C doesn't support SFINAE.

Thread Thread
 
emincin profile image
Emin Cin

Yes C doesn't support SFINAE, but with your tricks, we can do it :), at least we could simulate it.

Thread Thread
 
emincin profile image
Emin Cin • Edited

SFINAE in C, here we are:

#include <assert.h>
#include <stdio.h>

#define let auto

#define constexpr(EXPR) ( STATIC_IF( (EXPR), 1, 0 ) )

#define is_same(T1, T2) _Generic((T1){ 0 }, \
  typeof_unqual(T2): 1, \
  default: 0)

#define get_if_type(expr, T) _Generic((expr), \
  T: (expr), \
  default: (T){ 0 })

// SFINAE simulation (no compile-time error)
  int x = 42;
  let a = &x;
  //let a = 10;
  if constexpr (is_same(typeof(a), int)) {
    let value = get_if_type(a, int);
    printf("%d\n", value);
  }
  if constexpr (is_same(typeof(a), int*)) {
    let value = get_if_type(a, int*);
    assert(value != NULL);
    printf("%d\n", *value);
  }
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Except you're not simulating SFINAE. If the code compiles, then the code is valid in all substitutions. If at least one substitution is invalid C, the code won't compile — it's not ignored. Period.

You can call that "simulating SFINAE" if you want, but that doesn't change reality.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas • Edited

BTW, this:

if constexpr ( IS_SAME( T, U ) )
Enter fullscreen mode Exit fullscreen mode

can be reduced to:

if ( IS_SAME( T, U ) )
Enter fullscreen mode Exit fullscreen mode

since IS_SAME is already a constant expression. All constexpr does is ensure it's a constant expression — which in this case it is, but it doesn't change the result.

In general, if constexpr is pointless when you know the argument is already a constant expression. The only case where if constexpr might be useful is in a function-like macro that's passed an expression EXPR. In that case, you don't know in advance whether EXPR is a constant expression.

Thread Thread
 
emincin profile image
Emin Cin

I agree with you, constexpr in here is just a pointless stuff.
Basically I use constexpr as a constraint, requiring the expression to be constant expression (it can be used as a trait in some macros).
Otherwise, it's no different from a regular if statement.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Actually, constexpr doesn't work for non-constant expressions: the compilation fails, so if constexpr doesn't work at all, really.

The problem is that STATIC_IF requires EXPR to be a constant expression — which is fine. The point of STATIC_IF is to do either the THEN or ELSE part based on whether the value of the constant expression is non-zero or zero — not whether EXPR is a constant expression.

Collapse
 
gberthiaume profile image
G. Berthiaume • Edited

One problem I'm encountering when trying to build generic API is my inability to detect if a struct has a member—something like a HAS_ATTRIBUTE macro.

For example, we could redefine STRLEN by leveraging your STATIC_IF and this hypothetical HAS_ATTRIBUTE macro:

#define STRLEN(S)  STATIC_IF( HAS_ATTRIBUTE((S), .len ),                   \
                             (S).len,                                      \
                             (_Generic( (S),                               \
                                char const* : strlen( (char const*)(S) ),  \
                                default: 0))                                            
Enter fullscreen mode Exit fullscreen mode

This example is a bit naive, but this kind of pattern would be useful for building, let's say a linear algebra library.

Thanks for your writing Paul, I'm returning to this article because it's a fantastic read.

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

Being able to determine whether a struct has a specific member is beyond the capabilities of the preprocessor — it doesn't understand C. And you can't use _Generic since you could never check for something (like a struct member) not existing because the only way you could know that is by compiling a small piece of test code that uses said member: if it compiles, it exists — but if it does not exist, then the program will fail to compile.

Typically, such things are done at "configure" time. For example, autoconf has AC_CHECK_MEMBER that then defines a macro (or not) based on the result.

I use it in one of my projects to check whether struct passwd contains a pw_dir member: if so, autoconf defines HAVE_STRUCT_PASSWD_PW_DIR for which you can then use #if or #ifdef (for example).

BTW, __has_attribute exists, but tests whether the compiler supports a particular attribute, not whether a struct has a specific member.

BTW2, I occasionally retroactively add stuff to this article, e.g., I recently added IS_SAME().

Collapse
 
gberthiaume profile image
G. Berthiaume

Typically, such things are done at "configure" time.

Thanks that makes sense. If you can add a build step Autoconf seems like the perfect solution for this.
That said, I will continue my research, after all, the macro world is full of surprises

but tests whether the compiler supports a particular attribute, not whether a struct has a specific member.

You're right. My mistake. I was thinking about zig's @hasField.

BTW2, I occasionally retroactively add stuff to this article, e.g., I recently added IS_SAME().

That's great ! Maybe this topic (c macro, API design) is worth its own series. :)

As always, thanks for your answer!

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

That preprocessor iceberg is quite something! When I have more time, I'll have to go through it in detail.

Thread Thread
 
gberthiaume profile image
G. Berthiaume

I knew you'd like it :)

Some of them are fascinating. That said, they are a bit too magical for using in a codebase, IMO.

Here are some of my favorites:

Collapse
 
ericraible profile image
Eric Raible

Lots of great stuff in this article, thanks! But perhaps a cleaner approach to the lack of SFINAE could be:

#define STRLEN(s)                                   \
  _Generic(s,                                       \
    char *   : strlen(ONLY_IF_T(char *, s, "")),    \
    strbuf * : ONLY_IF_T(strbuf *, s, 0)->len)

#define ONLY_IF_T(type, val, fail)                  \
    ((type)_Generic((val), type : (val), default : (fail)))


Enter fullscreen mode Exit fullscreen mode
Collapse
 
pauljlucas profile image
Paul J. Lucas

What makes that cleaner?

Collapse
 
ericraible profile image
Eric Raible

In my view it is cleaner because it treats each type identically, and doesn't require an extra dummy function.

It's unfortunate that either technique requires that each clause to repeat the type name but I don't see any way around that.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

But does it have the same mistake-catching ability? My implementation will result in an undefined function at link-time. Your implementation won't.

In this particular case, if the ONLY_IF_T(strbuf*,...) case ends up being selected because the programmer made a mistake, then the result would be using 0 (a null pointer) for ->len and the program crashing at run-time rather than link-time, no?

Thread Thread
 
ericraible profile image
Eric Raible

I don't think so, for the same reason that yours can safely call strlen((const chr *)(S)) even when the type is strbuf *.

But perhaps you're imagining a different type of mistake? I would have no problem with either technique failing if the programmer is selecting for type T and then casts the value to some different type T1.

On another note, tcc seems to allow SFINAE (at least sometimes). It's so much more pleasant, for the life of me I don't understand why "they" wrote the standard as they did...

Thread Thread
 
pauljlucas profile image
Paul J. Lucas • Edited

For the strlen( (char const*)s ), case, perhaps I was a bit lazy. You could use ONLY_IF_T() there as well so its check would be for both cases.

I don't have the link handy, but I remember reading that the reason SFINAE isn't allowed in C is because it would have been adding a whole new concept that would be used only by _Generic and nowhere else — and the committee thought that was too much. In C++, SFINAE was added as part of templates — a large feature — since circa 1990.

Collapse
 
gberthiaume profile image
G. Berthiaume

I've been trying to put this article teaching in pratices by building an IS_WITHIN macro that would not generate a warning when used with a unsigned number.

// Naive implementation
#define IS_WITHIN(_number_, _min_, _max_) ((_number_) <= (_max_)) && ((_number_) >= (_min_))
uint8_t x = 3;
printf("%d\n", IS_WITHIN(x, 0, 12));
//  
// warning: comparison is always true due to limited range of data type [-Wtype-limits]
Enter fullscreen mode Exit fullscreen mode

My implementation looks like this.

#define IS_WITHIN(_number_, _min_, _max_)              \
    STATIC_IF(IS_UNSIGNED(_number_) && (_min_ == 0),   \
                ((_number_) <= (_max_)),               \
                ((_number_) <= (_max_)) && ((_number_) >= (_min_)))
Enter fullscreen mode Exit fullscreen mode

Saddly, this macro has the same problem as STRLEN: both STATIC_IF branches is compiled and therefore the warning is still generated. Using ONLY_IF_T doesn't seems to work because of the lack of functions.

Does anybody has an idea on how to create a STATIC_IF that ignores the invalid condition?

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

You don't need STATIC_IF:

#define IS_WITHIN(N,MIN,MAX)     (((N) > (MIN) || (N) == (MIN)) && (N) <= (MAX))
Enter fullscreen mode Exit fullscreen mode

This compiles with no warnings for unsigned types, at least with my compiler. The trick is to split >= into > || ==. Neither subexpression is always true and the compiler doesn't realize that the combination is always true when MIN is 0.

Don't fall into the trap of trying to use things like STATIC_IF where they're really not needed and overcomplicating the solution.

Collapse
 
gberthiaume profile image
G. Berthiaume • Edited

Hi Paul,
Thanks for you reply.

I just tested your solution with

  • gcc with -Wall -Wextra -Wpedantic -Wconversion
  • clang with -Wall -Wextra -Wpedantic -Wconversion
  • msvc with /W4 and everything looks great: no warning generated.

Don't fall into the trap of trying to use things like STATIC_IF where they're really not needed and overcomplicating the solution.

You're absolutly rigth. I try to have as little complex macros as possible.

I didn't think about spliting the operator in two.
To be honest, I'm supprise this even works.

Best,
Gabriel

Collapse
 
emincin profile image
Emin Cin

Hello Paul!
Thank you so much for sharing valuable stuffs.

I noticed that IS_ARRAY_EXPR doesn't work correctly in MSVC (in gcc/clang it works well)

Test case:

#define IS_ARRAY_EXPR(A)   \
  _Generic( &(A),          \
    typeof(*(A)) (*)[]: 1, \
    default           : 0  \
  )

  int arr[10] = { 0 };
  printf("%d\n", IS_ARRAY_EXPR(arr)); // output 0 in MSVC
Enter fullscreen mode Exit fullscreen mode

My fix:

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

#define is_array(x) _Generic(&(x), \
  typeof(*(x)) (*)[ARRAY_SIZE(x)]: 1, \
  default: 0 \
)

  int arr[10] = { 0 };
  printf("%d\n", is_array(arr)); // output 1 in MSVC and gcc/clang
Enter fullscreen mode Exit fullscreen mode

Looking forward to your reply!

Collapse
 
pauljlucas profile image
Paul J. Lucas

My original IS_ARRAY_EXPR seems to work just fine.

Collapse
 
emincin profile image
Emin Cin • Edited

Output is different from the expected value 1 check this out

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Hmm, yes. Both clang and gcc get it right. IMHO, msvc is wrong. But your work-around fixes it.

Collapse
 
emincin profile image
Emin Cin

I think defining IS_SAME_TYPE this way will make it look simpler.

#define IS_SAME_TYPE(T,U) \
  _Generic( (T){0},       \
    typeof_unqual(U): 1,  \
    default         : 0   \
  )
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

Your version doesn't work for either void or incomplete types like struct S;. However, neither does mine. A better version that works for all types including void is:

#define IS_SAME_TYPE(T,U)               \
  _Generic( (typeof_unqual(T)*)nullptr, \
    typeof_unqual(U)* : 1,              \
    default           : 0               \
  )
Enter fullscreen mode Exit fullscreen mode

You need to use pointers, not objects, since you can have a pointer to any type including void.

Collapse
 
emincin profile image
Emin Cin

When we need to strictly compare two types (const int and int), we can use IS_SAME_TYPE_STRICT to detect if their types are equal.

#define IS_SAME_TYPE_STRICT(T,U)        \
  _Generic( (typeof(T)*)nullptr,        \
    typeof(U)* : 1,                     \
    default           : 0               \
  )
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pauljlucas profile image
Paul J. Lucas

That wasn't my point. My point was that you need to use pointers to do the comparison, not objects.

Thread Thread
 
emincin profile image
Emin Cin

Yes, you are right. In order to support comparison of void type and incomplete types, we must use pointer instead of object.

Collapse
 
emincin profile image
Emin Cin

Without dereferencing a pointer, new IS_SAME_TYPE allows this:

#define IS_SAME_TYPE(T,U) \
  _Generic( (T){0},       \
    typeof_unqual(U): 1,  \
    default         : 0   \
  )

void test(void) {}

  printf("%d\n", IS_SAME_TYPE(typeof(&test), void(*)(void))); // output 1
  printf("%d\n", IS_SAME_TYPE(void(*)(void), typeof(&test))); // output 1
Enter fullscreen mode Exit fullscreen mode