Introduction
Among other things, C11 added the _Generic keyword that enables compile-time selection of an expression based on the type o...
For further actions, you may consider blocking this person and/or reporting abuse
For others reading, it’s not an error, but merely a warning. @notgiven chose to add
-Werror.Hi there,
There is a limit to
STATIC_IF: THEN/ELSE does not support statements.Based on your code I created
if_constexpr. Just like in C++, it can constrain the if condition to be determined at compile time.When compiler optimizations (-O1) are enabled, branches where the if_constexpr (0) will be automatically ignored.
Yes, I'm aware that
STATIC_IFhas limitations. Perhaps a better name would have beenSTATIC_IF_EXPRto 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_boolat all: just justSTATIC_IFas-is whereTHENis1andELSEis0.Your
if_constexprworks, but it's not clear it offers any new functionality since you could just do:That is, just use the normal
#if.The value of
STATIC_IFis precisely that it is an expression, not a statement.Ignore
comptime_boolit's just for fun :)#ifcan't handle something like this:Ah, yes, that's true. So
if_constexprbuilds upon (does not replace)STATIC_IF:Actually, you can hoist the
ifout of the macro and make it exactly likeif constexprin C++:The reason this works is because the preprocessor will not expand a function-like macro when not followed by
(. Hence, definingconstexpr(EXPR)as a macro does not “shadow” the realconstexprin C23 that still works. Therefore, you can really writeif constexprwith a space between them just like in C++.Woww, this looks really cool. Now I'm trying to write some SFINAE code with
if constexpr.As I explained in the article, C doesn't support SFINAE.
Yes C doesn't support SFINAE, but with your tricks, we can do it :), at least we could simulate it.
SFINAE in C, here we are:
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.
BTW, this:
can be reduced to:
since
IS_SAMEis already a constant expression. Allconstexprdoes is ensure it's a constant expression — which in this case it is, but it doesn't change the result.In general,
if constexpris pointless when you know the argument is already a constant expression. The only case whereif constexprmight be useful is in a function-like macro that's passed an expressionEXPR. In that case, you don't know in advance whetherEXPRis a constant expression.I agree with you,
constexprin here is just a pointless stuff.Basically I use
constexpras 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
ifstatement.Actually,
constexprdoesn't work for non-constant expressions: the compilation fails, soif constexprdoesn't work at all, really.The problem is that
STATIC_IFrequiresEXPRto be a constant expression — which is fine. The point ofSTATIC_IFis to do either theTHENorELSEpart based on whether the value of the constant expression is non-zero or zero — not whetherEXPRis a constant expression.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_ATTRIBUTEmacro.For example, we could redefine
STRLENby leveraging yourSTATIC_IFand this hypotheticalHAS_ATTRIBUTEmacro: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.
Being able to determine whether a
structhas a specific member is beyond the capabilities of the preprocessor — it doesn't understand C. And you can't use_Genericsince you could never check for something (like astructmember) 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_MEMBERthat then defines a macro (or not) based on the result.I use it in one of my projects to check whether
struct passwdcontains apw_dirmember: if so, autoconf definesHAVE_STRUCT_PASSWD_PW_DIRfor which you can then use#ifor#ifdef(for example).BTW,
__has_attributeexists, but tests whether the compiler supports a particular attribute, not whether astructhas a specific member.BTW2, I occasionally retroactively add stuff to this article, e.g., I recently added
IS_SAME().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
You're right. My mistake. I was thinking about zig's
@hasField.That's great ! Maybe this topic (c macro, API design) is worth its own series. :)
As always, thanks for your answer!
That preprocessor iceberg is quite something! When I have more time, I'll have to go through it in detail.
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:
Lots of great stuff in this article, thanks! But perhaps a cleaner approach to the lack of SFINAE could be:
What makes that cleaner?
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.
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 using0(a null pointer) for->lenand the program crashing at run-time rather than link-time, no?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...
For the
strlen( (char const*)s ), case, perhaps I was a bit lazy. You could useONLY_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
_Genericand 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.I've been trying to put this article teaching in pratices by building an
IS_WITHINmacro that would not generate a warning when used with a unsigned number.My implementation looks like this.
Saddly, this macro has the same problem as
STRLEN: bothSTATIC_IFbranches is compiled and therefore the warning is still generated. UsingONLY_IF_Tdoesn't seems to work because of the lack of functions.Does anybody has an idea on how to create a
STATIC_IFthat ignores the invalid condition?You don't need
STATIC_IF: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 whenMINis0.Don't fall into the trap of trying to use things like
STATIC_IFwhere they're really not needed and overcomplicating the solution.Hi Paul,
Thanks for you reply.
I just tested your solution with
-Wall -Wextra -Wpedantic -Wconversion-Wall -Wextra -Wpedantic -Wconversion/W4and everything looks great: no warning generated.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
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:
My fix:
Looking forward to your reply!
My original
IS_ARRAY_EXPRseems to work just fine.Output is different from the expected value 1 check this out
Hmm, yes. Both clang and gcc get it right. IMHO, msvc is wrong. But your work-around fixes it.
I think defining
IS_SAME_TYPEthis way will make it look simpler.Your version doesn't work for either
voidor incomplete types likestruct S;. However, neither does mine. A better version that works for all types includingvoidis:You need to use pointers, not objects, since you can have a pointer to any type including
void.When we need to strictly compare two types (
const intandint), we can useIS_SAME_TYPE_STRICTto detect if their types are equal.That wasn't my point. My point was that you need to use pointers to do the comparison, not objects.
Yes, you are right. In order to support comparison of
voidtype and incomplete types, we must usepointerinstead ofobject.Without dereferencing a pointer, new
IS_SAME_TYPEallows this: