6 ms·
Parameterized types in C using the new tag compatibility rule
- unwind 1y agoI think this is an interesting change, even though I (as someone who has loved C for 30+ years and use it daily in a professional capacity) don't immediately see a lot of use-cases I'm sure they can be found as the author demonstrates. Cool, and a good post!
- deleted 1y ago[deleted]
- glouwbug 1y agoCombined with C23's auto (see vec_for) you can technically backport the entirety of C++'s STL (of course with skeeto's limitation in his last paragraph in mind). gcc -std=c23. It is a _very_ useful feature for even the mundane, like resizable arrays: #include <stdlib.h> #include <stdio.h> #define vec(T) struct { T* val; int size; int cap; } #define vec_push(self, x) { \ if((self).size == (self).cap) { \ (self).cap = (self).cap == 0 ? 1 : 2 * (self).cap; \ (self).val = realloc((self).val, sizeof(*(self).val) * (self).cap); \ } \ (self).val[(self).size++] = x; \ } #define vec_for(self, at, ...) \ for(int i = 0; i < (self).size; i++) { \ auto at = &(self).val[i]; \ __VA_ARGS__ \ } typedef vec(char) string; void string_push(string* self, char* chars) { if(self->size > 0) { self->size -= 1; } while(*chars) { vec_push(*self, *chars++); } vec_push(*self, '\0'); } int main() { vec(int) a = {}; vec_push(a, 1); vec_push(a, 2); vec_push(a, 3); vec_for(a, at, { printf("%d\n", *at); }); vec(double) b = {}; vec_push(b, 1.0); vec_push(b, 2.0); vec_push(b, 3.0); vec_for(b, at, { printf("%f\n", *at); }); string c = {}; string_push(&c, "this is a test"); string_push(&c, " "); string_push(&c, "for c23"); printf("%s\n", c.val); }
- int_19h 1y agoWhat I don't quite get is why they didn't go all the way in and basically enabled full fledged structural typing for anonymous structs.
- uecker 1y agoThat way my plan, but the committee had concerns about type safety.
- int_19h 1y agoThis would probably need some special rules around stuff like: typedef struct { ... } foo_t; typedef struct { ... } bar_t; foo_t foo = (bar_t){ ... }; i.e. these are meant to be named types and thus should remain nominal even though it's technically a typedef. And ditto for similarly defined pointer types etc. But this is a pattern regular enough that it can just be special-cased while still allowing proper structural typing for cases where that's obviously what is intended (i.e. basically everywhere else).
- uecker 1y agoI agree, that is also the solution I suggested. I will try to bring this back for C2y.
- Surac 1y agoi fear this will make slopy code compile more often OK.
- ioasuncvinvaer 1y agoCan you give an example?
- poly2it 1y agoDear God I hope nobody is committing unreviewed LLM output in C codebases.
- rwmj 1y agoSlighty off-topic, why is he using ptrdiff_t (instead of size_t) for the cap & len types?
- r1chardnl 1y agoFrom one of his other blogposts. "Guidelines for computing sizes and subscripts" Never mix unsigned and signed operands. Prefer signed. If you need to convert an operand, see (2). https://nullprogram.com/blog/2024/05/24/ https://nullprogram.com/blog/2024/05/24/ https://www.youtube.com/watch?v=wvtFGa6XJDU https://www.youtube.com/watch?v=wvtFGa6XJDU
- poly2it 1y agoI still don't understand how these arguments make sense for new code. Naturally, sizes should be unsigned because they represent values which cannot be unsigned. If you do pointer/size arithmetic, the only solution to avoid overflows is to overflow-check and range-check before computation. You cannot even check the signedness of a signed size to detect an overflow, because signed overflow is undefined! The remaining argument from what I can tell is that comparisons between signed and unsigned sizes are bug-prone. There is however, a dedicated warning to resolve this instantly. It makes sense that you should be able to assign a pointer to a size. If the size is signed, this cannot be done due to its smaller capacity. Given this, I can't understand the justification. I'm currently using unsigned sizes. If you have anything contradicting, please comment :^)
- sim7c00 1y agoI dont know either. int somearray[10]; new_ptr = somearray + signed_value; or element = somearray[signedvalue]; this seems almost criminal to how my brain does logic/C code. The only thing i could think of is this: somearray+=11; somearray[-1] // index set to somearray[10] ?? if i'd see my CPU execute that i'd want it to please stop. I'd want my compiler to shout at me like a little child, and be mean until i do better. -Wall -Wextra -Wextra -Wpedantic <-- that should flag i think any of these weird practices. As you stated tho, i'd be keen to learn why i am wrong!
- tialaramex 1y agoIt seems as though this makes it impossible to do the new-type paradigm in C23 ? If Goose and Beaver differ only in their name, C now thinks they're the same type so too bad we can tell a Beaver to fly even though we deliberately required a Goose ?
- yorwba 1y ago"Tag compatibility" means that the name has to be the same. The issue the proposal is trying to address is that "struct Goose { float weight; }" and "struct Goose { float weight; }" are different types if declared in different locations of the same translation unit, but the same if declared in different translation units. With tag compatibility, they would always be treated as being the same. "struct Goose { float weight; }" and "struct Beaver { float weight; }" would remain incompatible, as would "struct { float weight; }" and "struct { float weight; }" (since they're declared without tags.)
- tialaramex 1y agoAh, thanks, that makes sense.
- fuhsnn 1y agoThe recent #def #enddef proposal[1] would eliminate the need for backslashes to define readable macros, making this pattern much more pleasant, finger crossed for its inclusion in C2Y! [1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3531.txt https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3531.txt
- cb321 1y agoWhile long-def's might be nice, you can even back in ANSI C 89 get rid of the backslash pattern (or need to cc -E and run through GNU indent/whatever) by "flipping the script" and defining whole files "parameterized" by their macro environment like https://github.com/c-blake/bst https://github.com/c-blake/bst or https://github.com/glouw/ctl/ https://github.com/glouw/ctl/ Add a namespacing macro and you have a whole generics system, unlike that in TFA. So, it might add more value to have the C std add an `#include "file.c" name1=val1 name2=val2` preprocessor syntax where name1, name2 would be on a "stack" and be popped after processing the file. This would let you do types/functions/whatever "generic modules" with manual instantiation which kind of fits with C (manual management of memory, bounds checking, etc.) but preprocessor-assisted "macro scoping" for nested generics. Perhaps an idea to play with in your slimcc fork?
- glouwbug 1y agoI've been thinking of maybe doing CTL2 with this. Maybe if #def makes it in.
- cb321 1y agoI think the #include extension could make vec_vec / vec_list / lst_str type nesting more natural/maybe more general, but maybe just my opinion. :-) I guess ctags-type tools would need updating for the new possible definition location. Mostly someone needs to decide on a separation syntax for stuff like `name1(..)=expansion1 name2(..)=expansion2` for "in-line" cases. Compiler programs have had `cc -Dname(..)=expansion` or equivalents since the dawn of the language, but they actually get the OS/argv idea of separation from whatever CL args or Windows APIs or etc. Anyway, might makes sense to first get experience with a slimcc/tinycc/gcc/clang cpp++ extension. ;-) Personally, these days I mostly just use Nim as a better C.
- JonChesterfield 1y agoNot personally interested in this hack, but https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3037.pdf https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3037.pdf means struct foo {} defined multiple times with the same fields in the same TU now refers to the same thing instead of to UB and that is a good bugfix.
- IAmLiterallyAB 1y agoIf you're reaching for that hack, just use C++? You don't have to go all in on C++-isms, you can always write C-style C++ and only use the features you need.
- waynecochran 1y agoNot always a viable option -- especially for embedded and systems programming.
- _proofs 1y agoi work in an embedded space in the context of devices and safety. if it were as simple as "just use c++ for these projects" most of us would use a subset, and our newer projects try to make this a requirement (we roll our own ETL for example). however for some niche os specific things, and existing legacy products where oversight is involved, simply rolling out a c++ porting of it on the next release is, well, not a reality, and often not worth the bureaucratic investment. while i have no commentary on the post because i'm not really a c programmer, i think a lot of comments forget some projects have requirements, and sometimes those requirements become obsolete, but you're struck with what you got until gen2, or lazyloading standardization across teams.
- Too 1y agoIn embedded you are typically stuck on some ancient proprietary compiler and can't take advantage of the latest C versions. Even less so if you need safety standards like MISRA. That of course doesn't help you with the switch away from C. The question is why they keep updating the language. The only ones with valid reasons to not upgrade to some more sane language can't take advantage of the new features.
- pton_xd 1y agoYeah as someone who writes C in C++, everytime I see posts bending over backwards trying to fit parameterized types into C I just cringe a little. I understand the appeal of sticking to pure C, but... why do that to yourself? Come on over, we've got lambdas, and operator overloading for those special circumstances... the water's fine!
- uecker 1y agoHere is my experimental library for generic types with some godbolt links to try: https://github.com/uecker/noplate https://github.com/uecker/noplate
- o11c 1y agoAre we getting a non-broken `_Generic` yet? Because that's the thing that made me give up with disgust the last project I tried to write in C. Manually having to do `extern template` a few times is nothing in comparison.
- uecker 1y agoWhat is a non-broken `_Generic' ?
- o11c 1y agoA `_Generic` that only requires its expressions to be valid for the type associated with them, rather than spewing errors everywhere.
- uecker 1y agoThis is intentional. But my idea for _Generic is to have _Generic(x, int i: i + 1, float f: f + 1.); where the i and f then have the correct type, so you do not need to refer to 'x' in those expressions.
- deleted 1y ago[deleted]
- Arnavion 1y agoNeat similarity to Zig's approach to generic types. The generic type is defined as a type constructor, a function that returns a type. Every instantiation of that generic type is an invocation of that function. So the generic growable list type is `fn ArrayList(comptype T: type) type` and a function that takes two lists of i32 and returns a third is `fn foo(a: ArrayList(i32), b: ArrayList(i32)) ArrayList(i32)`
- deleted 1y ago[deleted]
- wsve 1y agoSometimes I look at the way C macros are used to simulate generics and wonder to myself... Why don't y'all just put templates into the standard? If the way you're writing C code is by badly imitating C++, then just imitate C++! There's no shame in it!
- jimbob45 1y agoC++ doesn’t force you to pay for anything you don’t use so you can just use the C++ compiler at that point and change the few incompatibilities between C and C++. That said…I agree that there is a lot of syntactic sugar that could be added for free to C.
- uecker 1y agoMaybe you could try to formulate it what sense this approach is actually inferior? IMHO it is superior to C++ templates by being far simpler.