Live data from Hacker News

Workarounds for C11 _Generic()

chiark.greenend.org.uk

41–50 of 58 posts

Re: Workarounds for C11 _Generic()

#41

The only reason I can imagine for this behaviour is that the compiler/standard writers did not want it. Maybe C just shouldn't include generics. Especially not as part of the macro layer.

As a member of the WG14 I can tell you that _Generic does cause a fair bit of issues and complications in the language, and as a user of C i think _Generic is bad, because it mostly useful for confusing users about what code does so, Yes C would be better off without _Generic. Please don't use it.

[deleted]

Re: Workarounds for C11 _Generic()

#42

Earlier quoted context omitted.

As a member of the WG14 I can tell you that _Generic does cause a fair bit of issues and complications in the language, and as a user of C i think _Generic is bad, because it mostly useful for confusing users about what code does so, Yes C would be better off without _Generic. Please don't use it.

_Generic allows for more complex assertions in static_assert which is really useful for correctness (For example, your embedded chip vendor offers an API where uint8_t is used to transfer bytes using a protocol, so you have to assert that uint8_t is either char or unsigned char in order to prove that there will be no strict aliasing violation during pointer casting). Also, it's useful in order to implement the length…

There are always uses for every feature, thats how you end up with C++. The question is if it is worth the implementation burden, the complexity, making the language harder to learn and read, and the potential intentional and unintentional abuse. IMO it is clearly not.

Re: Workarounds for C11 _Generic()

#43

Earlier quoted context omitted.

Macros are perfectly capable of static polymorphism. That is one of their primary use cases. _Generic is simply an alternative mechanism for static polymorphism, with some extra capabilities and some limitations in comparison to your typical macro.

To prove your assertion, would you mind defining a length() function via macros that works with both a char * and a pointer to a struct {int len; char *data}, as the fine article explained?

There is a mixup here between different flavours of polymorphism.

_Generic does ad-hoc polymorphism via type-based dispatch in order to implement function overloading.

Macros are more like a limited form of parametric polymorphism, in that they are polymorphic up to what is allowed by the operations they apply to their arguments. In C that typically means relying on implicit or explicit coercions, or C’s mildly overloaded operators.

Re: Workarounds for C11 _Generic()

#44
post #21

Earlier quoted context omitted.

The macros in tgmath and more recently stdbit show why those could be necessary. If you have a set of functions for addition with overflow detection, say add_overflow{i,l,ll}, and you have a pair of ptrdiff_t’s or int32_t’s or whatnot that you know are standard integer types, and you want to use the appropriate add_overflow* function, can you do it? With _Generic you can. Without it I think you’re stuck providing sep…

You make it sound alot more complicated. Ignoring the library functions or stuff you can include from safe coding standard headers. You just do something like. BOOL AddOverflowUnsigned(BYTE* a, BYTE* b, INT32 sizea, INT32 sizeb) { BOOL IsMsb = PlatformIsMSB(); BOOL IsLsb = PlatformIsLSB(); //log error if(!IsMsb && !IsLsb) return FALSE; //Early out for overflow. Assuming if(sizea == sizeb && PlatformIsMSB()) { BOOL AM…

This is OK as a fallback, even if it's not at all strictly compliant (the standard still allows PDP or Honeywell endian and padding bits wherever).

As the main option it's extremely silly, though, when the entirety of the function on e.g. x86-64 could be (using the GCC signature but MS calling convention and syntax for according to your apparent preference)

  ; bool add_overflowll(long long x, long long y, long long *result)

  add_overflowll PROC
      add rcx, rdx
      seto eax
      mov qword ptr [r8], rcx
      ret
  add_overflowll ENDP
for a total of no loops, no conditional branches, and three instructions with no branches at all when inlined (not shown because I don't remember the MS inline assembly syntax). Why check beforehand when the CPU does the check for you on every arithmetic operation and all you need to do is to ask it for the result? (Unless your CPU is RISC-V because RISC-V.)

And of course the newfangled standard functions are size-dependent (more like mine than yours), so either you still need _Generic or essentially equivalent compiler-specific magic, or you have to introduce an ABI dependency on which integer type your ptrdiff_t or ssize_t or off_t or whatever typedef you got from a random place in a library or OS header actually ends up being.

Re: Workarounds for C11 _Generic()

#45
post #21

Earlier quoted context omitted.

The macros in tgmath and more recently stdbit show why those could be necessary. If you have a set of functions for addition with overflow detection, say add_overflow{i,l,ll}, and you have a pair of ptrdiff_t’s or int32_t’s or whatnot that you know are standard integer types, and you want to use the appropriate add_overflow* function, can you do it? With _Generic you can. Without it I think you’re stuck providing sep…

You make it sound alot more complicated. Ignoring the library functions or stuff you can include from safe coding standard headers. You just do something like. BOOL AddOverflowUnsigned(BYTE* a, BYTE* b, INT32 sizea, INT32 sizeb) { BOOL IsMsb = PlatformIsMSB(); BOOL IsLsb = PlatformIsLSB(); //log error if(!IsMsb && !IsLsb) return FALSE; //Early out for overflow. Assuming if(sizea == sizeb && PlatformIsMSB()) { BOOL AM…

First of all, you forgot to return the actual sum from the function. Second, original add_overflow-style functions support arbitrary expressions as input arguments, not just pointers to l-values. And third, there is no way all this stuff is going to get optimized down to inlined

    add     rdi, rsi
    setc    eax
    mov     [rdx], rdi

Re: Workarounds for C11 _Generic()

#46
post #4
post #2

According to this the "big bug" is that... _Generic works mostly like a macro and expands code that the compiler sees. That seems like a little weak, macros have been doing this forever via a mere extra level of indirection. So sure, "(x)->length" might not be valid syntax in all configurations the compiler might see. But "LENGTH(x)" is, e.g.: #if X_MIGHT_BE_MYSTRINGBUFFER #define LENGTH(x) ((x)->length) #else #defin…

Macros and #ifdef do not solve the problems that _Generic is used for. _Generic is for static polymorphism, not for target or build configuration.

> Macros and #ifdef do not solve the problems that _Generic is used for.

No, but they solve the specific "big bug" described in the article very well. Use _Generic for what it's good for. Use macros to fill in the gaps (in this case build/preprocessor configuration differences that change the syntax used in the RHS of the generic expansion).

Again, this is C. You want fancy type systems, you know where to find them. Don't ask for them here, we don't want them.

Re: Workarounds for C11 _Generic()

#47

Earlier quoted context omitted.

> but is that a useful feature? It would solve the usability issues experienced by Simon Tatham, described in his article. For instance, if one of the clauses has x->foo, it wouldn't matter that the actual x argument in a given instance is char *, that cannot be dereferenced as x->foo. It would be up to the programmer to test every one of the supported cases of their _Generic construct. One way to avoid problems woul…

> It would solve the usability issues experienced by Simon Tatham, described in his article. In a very overkill way, though. You can solve the issue in the article by parsing the expression but not doing anything with it. You don't need to allow invalid syntax for that problem. So to be clear, I'm asking specifically about whether it's useful to allow invalid syntax inside _Generic. What does that allow in practical…

It's easy to implement and obviously correct. I can't think of a situation in which it would not work, or on which it would be a nuisance (prevent the programmer from doing something sensible).

I don't know that about the implementation which parses the dead code. Implementing it could be troublesome in some way. In any compiler that checks constraints while parsing, implementing parsing without constraint checking means going to all those places and checking a flag.

Actually, it occurs to me that none of this is necessary. How the construct can work is by pretending that the argument in the dead cases has the type of the left side of the association list entry:

So if we have

  _Generic(x,
           char * : puts(x),
           dev * : puts(x->devname));
which is invoked with x being "abc", the dead dev * clause should be parsed and type checked under the pretense that there is a dev *x declaration in scope. And not that x is of type char *.

The implementation should be, anyway, generating a hidden local variable (gensym) to take the value of the expression.

We can imagine a code transformation like this (using GCC brace syntax):

  _Generic(x,
           char * : ({ char *__g0025 = x; puts(__g0025); }),
           dev * : (({ dev *__g0025 = x; puts(__g0025->devname); }))
Now here, we just have to suppress type checking of the dev *__g0025 = x; declaration where the only problem is that x is incompatible with __g0025. (That declaration wouldn't even have to be source code: it can be injected at the AST level.)

While this is all cool, my bracket counting idea is much simpler and more robust toward cases like different syntactic extensions being used from different compilers or other unforseen snags.

Re: Workarounds for C11 _Generic()

#48
post #46
post #4

Earlier quoted context omitted.

Macros and #ifdef do not solve the problems that _Generic is used for. _Generic is for static polymorphism, not for target or build configuration.

> Macros and #ifdef do not solve the problems that _Generic is used for. No, but they solve the specific "big bug" described in the article very well. Use _Generic for what it's good for. Use macros to fill in the gaps (in this case build/preprocessor configuration differences that change the syntax used in the RHS of the generic expansion). Again, this is C. You want fancy type systems, you know where to find them.…

> You want fancy type systems, you know where to find them. Don't ask for them here, we don't want them.

But the problem here is caused by the type system being overly aggressive. If it wasn't trying to force the wrong type onto unused code, everything would work fine.

Re: Workarounds for C11 _Generic()

#49
post #46

Earlier quoted context omitted.

> Macros and #ifdef do not solve the problems that _Generic is used for. No, but they solve the specific "big bug" described in the article very well. Use _Generic for what it's good for. Use macros to fill in the gaps (in this case build/preprocessor configuration differences that change the syntax used in the RHS of the generic expansion). Again, this is C. You want fancy type systems, you know where to find them.…

> You want fancy type systems, you know where to find them. Don't ask for them here, we don't want them. But the problem here is caused by the type system being overly aggressive. If it wasn't trying to force the wrong type onto unused code, everything would work fine.

Indeed. So? Again if you want to code in Haskell or Rust, those are fine choices. What you want, you can't have absent a complete rework of C's type system. And you don't actually want that.

Re: Workarounds for C11 _Generic()

#50

Earlier quoted context omitted.

Generics are fine. Its useful to have some introspection into the data that compiler has. Thats the whole point of macros in the first place. The behavior with _Generic is basically emergent behavior from implementation of macro processors. Macro replacements occur prior to actual compilation, so no compiler context exists for x, as such all code paths must be valid. Its much easier to require the programmer typecast…

> Macro replacements occur prior to actual compilation, so no compiler context exists for x, as such all code paths must be valid. How is it throwing an error like "invalid type argument of ‘->’" if there's no compiler context? And why does an error like that get in the way of macro replacements? (That might be the wrong message but the article says you at least get an equivalent.)

I mean in the sense of expanding the macro. It doesn't look at the type of operand at the macro expansion step, it simply replaces it with the _Generic() function call, which contains all the expressions, and each expression is parsed, and thus must be valid.
Post reply on HN