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
 
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
 
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
 
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
 
notgiven profile image
Notgranted

About TO_VOID_PTR, you might wish to see this StackOverflow question I asked related to it: error: pointer/integer type mismatch in conditional expression.

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.