62 ms·
Workarounds for C11 _Generic()
- teo_zero 3y agoThe "big bug" indeed limits the possibilities of _Generic. But if you really want to abuse it beyond its intended scope, I think you can simply add some casting, and not the gimmicks with nested _Generic. #define length(x) _Generic(x, \ char * : strlen((char*)(x)), \ String * : ((String*)(x))->len \ )
- ajross 3y agoAccording 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 #define LENGTH(x) 0 /* anything that converts to the output type will do */ #endif This is a routine pattern seen everywhere in C. The Linux kernel is filled with it, e.g. field accessors or arch-specific functions that are stubbed out when not needed, etc... Is this as clean as a full-on generic typesystem? No. But it's C, it shows a weirdness that you have to handle manually, and you do it the same way we've been doing it in C for decades. Not a new problem, doesn't need a new solution. It's C!
- fanf2 3y agoMacros and #ifdef do not solve the problems that _Generic is used for. _Generic is for static polymorphism, not for target or build configuration.
- ajross 3y ago> 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.
- Dylan16807 3y ago> 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.
- ajross 3y agoIndeed. 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.
- reichstein 3y agoBy all means work on getting `_Generic` removed again if that is what you want. As long as the feature is there, it should endeavor to be the best possible version of itself. Making it deliberately useless and unsuitable in order to avoid the obvious use of _doing different things for different types_ is really just having a suboptimal feature. And if it would make it a better feature to ignore the parts of the syntax, that are statically known to not be part of the program, it seems like low-hanging fruits. I'm sure there are philosophical reasons for every choice made in the design of this feature, but at the end of the day, users just see a feature being needlessly obtuse.
- ajross 3y ago> Making it deliberately useless How do you get "useless"? I literally showed five lines of code above that solve the putative problem with _Generic by leveraging an idiom we've been using for decades already. Meh. Use what you like. Clearly it's not C.
- Dylan16807 3y ago> I literally showed five lines of code above that solve the putative problem The top reply to your five lines literally points out why your solution doesn't solve the right problem. The macro you wrote puts the same definition everywhere. It can't even handle the original use case of calling a different function for each number type. If you're suggesting a combination approach of hiding the implementation with a macro until after _Generic gets resolved, I don't think that ordering is possible.
- mostlylurks 3y agoMacros 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.
- teo_zero 3y agoTo 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?
- fanf2 3y agoThere 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.
- kelnos 3y agoI think you've misunderstood the purpose of _Generic. Your #if statement will select exactly one implementation for every single site of the use of LENGTH() in the codebase, depending on the value of X_MIGHT_BE_MYSTRINGBUFFER at compile-time. _Generic() allows you to have both implementations available, and different implementations can be selected at the call site depending on the type of the argument passed.
- sigsev_251 3y agoIt's amazing how much power _Generic, typeof, typeof_unqual, auto and empty brace initializers can give to the compile time of C (that we already enjoy in C++ through templates, constexpr, auto and friends). Now, if only we had a way to write generic code in a reliable way...
- nstbayless 3y agoWill this work? define string_length(x) _Generic(x, \ const char * : strlen((const char*)(const void*)x), \ struct MyStringBuffer * : ((const MyStringBuffer*)(const void*)x)->length)
- kelnos 3y agoHeh, this was my first thought as well, and it does indeed compile and work (with GCC 12.2, anyway).
- panzi 3y agoYeah, this is what I saw in some example code. Also you don't need the (void*), at least not with clang or GCC trunk on compiler explorer: https://godbolt.org/z/a8rP91dKr https://godbolt.org/z/a8rP91dKr
- deleted 3y ago[deleted]
- Rexxar 3y agoNot directly related but does some c/c++ compiler implement a combination of flags that create a sort "c with templates" version of C ?
- constantcrying 3y agoJust use C++ with just templates?
- jimbob45 3y agoThat would be my advice too. What was the point of “You don’t pay for what you don’t use” if nobody’s going to use it?
- SAI_Peregrinus 3y agoThere's no common C++ build system. That means (among other things) that there's no way to turn features you don't use into compile-time errors. There's no project configuration to select an allowed subset of features. All the difficulty goes onto the programmers, and in a multi-person project into the code review process. In practice, you end up using the combined set of C++ sublanguages each contributor chooses to use. You have to know about all of C++'s features to use C++ in a large enough team.
- pjmlp 3y agoYes there is, with tooling like Sonar.
- deleted 3y ago[deleted]
- cmovq 3y agoTemplates are so intertwined with the C++ type system such that bringing "just templates" to C would require also bringing in a bunch of C++ features if you want templates to behave like they do in C++. Just for starters, you would need name mangling and/or function overloading.
- constantcrying 3y agoThe 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.
- mananaysiempre 3y agoThe 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 separate functions for every integer typedef in the standard library and then requiring all library authors to do the same for both integer typedefs and integer-accepting functions that they define. (_Generic is not part of the macro level, that’s why the semantic-checking issues discussed in the article even arise. The C preprocessor can still be implemented as a separate binary that doesn’t understand C itself, even in C23.)
- maldev 3y agoYou 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 AMsb = (a >> ((sizea * PlatformByteSize()) - 1)) & 1; BOOL BMsb = (b >> ((sizeb * PlatformByteSize()) - 1)) & 1; if(AMSB && BMsb) { //We overflow, early out. return FALSE; } else if(sizea == sizeb && PlatformIsLSB()) //Code here. //We add using bitwise operators so we can do it on any size. //SUM = A XOR B XOR CARRY, return in A //Impliment algo here. return TRUE; } int main() { UINT64 a = 69; UINT64 b = 420; if(sizeof(a) ! return AddOverflowUnsinged(&a, &b, sizeof(a), sizeof(b)); } But they have this in the STD now and also all the secure coding libs have this in it's header as a basic function. There's also the easier implementation of just adding a check to see if it will overflow, rather than add in the function. You also only have a few you can hardcode if you don't do it generic, since all will go back to the primitives of uint8, uint16 ....
- AlbertoGP 3y agoAt the bottom of the page lemonade is made: > Apart from the most obvious answer (that it’s useful for things exactly like <tgmath.h>), the best thing I’ve thought of to do with _Generic is to use it for deliberate error checking. > The annoyance in all the previous sections was that it was very hard to avoid compile errors, when we wanted our code to actually compile, run and do something useful. But if what we wanted was to provoke compile errors on a hair-trigger basis, then perhaps we could use it for that more reliably?
- dottedmag 3y agoSomehow C++ (and now C) standardization very often ends up with constructs that require a lot of ugly hacks in the code that uses them.
- camel-cdr 3y agoTiny C compiler actually has a "bug" in not implementing the "big bug". That is, the following expressions are cleanly compiled without any errors: _Generic(0, float: s/<[^>]*>/ /g, int: 1), _Generic(0, float: now the thing about this tcc implementaion is, int: 1), _Generic(0, float: that it just skips until the next comma, int: 1), _Generic(0, float: keeping track of parentheses nesting., int: 1), _Generic(0, float: [[ Suprizingly it doesnt validate which parentheses)), int: 1), _Generic(0, float: {{{so this bullshit is possible))), int: 1, default: #else #define this is great isnt it?); See: https://godbolt.org/z/o7jne7hWM https://godbolt.org/z/o7jne7hWM
- hgs3 3y agoIt sounds like the author misunderstands the purpose of _Generic. The author wants it to behave like pattern matching in ML languages, but that is not its purpose. The purpose of generic selection was to introduce function overloading [1] into C without breaking ABI compatibility. [1] https://en.wikipedia.org/wiki/Ad_hoc_polymorphism https://en.wikipedia.org/wiki/Ad_hoc_polymorphism
- deleted 3y ago[deleted]
- kazinator 3y agoI would have designed the feature like this: _Generic(<expr>, type1 : ( expr1 ), type2 : ( expr2 ), ... default : ( expr )) Here, the parentheses shown in this phrase pattern are required. The implementation would only parse and semantically analyze the expression of the matching type. For the others, the ( expr ) would be treated as a token sequence to be skipped, which has to contain valid tokens, and balancing parentheses, square brackets and braces. E.g. char *p = "foo" _Generic(p : int : ("foo" ++ / ? ([]&xyz) { ; } ), char * : (p[0])) Here, the type of p doesn't match int, and so the interior token sequence of ("foo" ++ / ? ([]&xyz) { ; } ) would just be scanned to check for balancing parentheses, brackets and braces, which allows the parser to locate the next clause in the association list.
- Dylan16807 3y agoThat would allow you to put syntactically invalid code into the expressions, but is that a useful feature? It's not very hard to have valid syntax in a situation like this. The main issue here is type enforcement.
- kazinator 3y ago> 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 would be to include only cases that are used in your codebase. Before adding a new case, add a use* of your generic macro with the currently unsupported type. That should fail to build. Then, add the case. (TDD style). The preprocessor's #ifdef allow syntactically invalid code. This is very useful; it lets you have conditional sections of code that are understood only by certain compilers due to using their extensions. Arguments to preprocessor macros, if not planted into syntax, can be syntactic gibberish, but parentheses have to balance. You cannot invoke a macro as mac({(}) and such. _Generic is meant to work with macros; it should have some macro-like forgiveness in it to make it as useful as possible. In Common Lisp, the #+ and #- syntax for conditionally skipping forms is similarly defined as doing a minimal parsing for balancing parentheses and a few other details. This allows #+ to switch between implementation-specific code that includes implementation-specific read syntax that cannot be read by all implementations. http://www.lispworks.com/documentation/lw51/CLHS/Body/02_dhq.htm http://www.lispworks.com/documentation/lw51/CLHS/Body/02_dhq... It works by binding *read-suppress* to true and then calling read to read the form.