10 ms·
I write type-safe generic data structures in C
- monkeyelite 1y agoAnother way is to not try to write generic data structures. When you tailor them to the use case you can simplify. The #1 data structure in any program is array.
- dwattttt 1y agoWhen all you have are arrays, everything looks like a problem you solve with arrays. There are quite a few problems that specialised containers are suited for, that's why they were created.
- monkeyelite 1y agoAnd you can write them when you need them. The situation where you need a red black tree with 10 different key/value combos isn’t real.
- dwattttt 1y agoYou could take away anything you use and say "but we could make it ourselves", that doesn't mean it's helpful.
- monkeyelite 1y agoExcept it’s very common for C programs to contain one-off data structures, so it’s not a hypothetical. It’s a concrete programming style.
- el_pollo_diablo 1y agoSure, but it is also very common for C programs to contain data structures that have one use in the program, and could still be instances of a generic type. You mentioned red black trees, which are a perfect example of that.
- dwattttt 1y agoDo you mean a data structure they only use once? Or one that's never been done elsewhere? If they only use it once, that seems like the worst effort/pay-off ratio you can get writing it yourself. And I don't think there's that many fundamental data structures out there... and even then, why would it be good to be forced to make your bespoke structure out of only arrays, when things like maps exist?
- monkeyelite 1y ago> And I don't think there's that many fundamental data structures out there No. There are a few fundamental ones which work well in a generic context. When you tailor make it you find simplifying assumptions. One I am aware of is a hash map that doesn’t need to delete individual keys.
- cb321 1y agoThis is true of the common tombstone approach to deletions in hash tables { which also require rehashing (like in resizing) if there are too many tombstones }. Somehow dissemination of Knuth v3,chapter6.4 Algorithm D has been weak, though it has lately become known as "backshift deletion" - unsure who coined that. It is limited to linear probing (but then these days that also creates the least memory traffic/potential latency). With this approach, there is no real specialized form of "hash tables that can delete". You may already know all of this and not disagreeing with anything. Just a natural follow-up.
- monkeyelite 1y agoIf we look at the major standard libraries for hash tables, you're telling me there will be no overhead to supporting the ability to delete keys? Isn't resizing logic already a counterexample?
- dwattttt 1y agoResizing has to happen regardless of the ability to delete a key, unless you know upfront how many entries you're going to add; I'd be much more worried about a home grown hash table's tuning to rebalance itself at say, 80% buckets full. If you _do_ know how many entries you're going to be adding upfront, you probably don't even want a hash table, you're probably better off with an array!
- el_pollo_diablo 1y agoIf, by "situation", you mean the development of a small program with so many constraints that using existing libraries is out if the question, then yes. Otherwise, that seems unwise to me. Not every user of a generic type has to be generic. A major selling point of generic types is that you write a library once, then everyone can instantiate it. Even if that is the only instance they need in their use case, you have saved them the trouble of reinventing the wheel. No colleague of mine may need 10 different instances of any of my generic libraries, but I bet that all of them combined do, and that our bosses are happy that we don't have to debug and maintain 10+ different implementations.
- monkeyelite 1y agoOk you’re telling me the upside which we already know. Now what’s the downside?
- el_pollo_diablo 1y agoYou mean the downside that we also already know, i.e. that there are some situations where a custom data structure would be superior for various reasons (e.g. smaller footprint)? Experienced programmers know when to reuse a generic library and when to roll out their own. We all know that. Yet you dismiss generic red black trees because there is no realistic single program that uses 10 key/value combinations. Not only is this false, but as I illustrated in my reply, a single program is not the relevant scope to decide whether to use a generic implementation. And as someone who has written a red black tree library, I am definitely in favor of reusing an implementation unless you have an excellent reason, which "I do not need 10 different instances in my program" or "my favorite language and its standard library only have arrays built in" most definitely are not.
- monkeyelite 1y ago> Yet you dismiss generic red black trees I use generic data structures all the time. That’s the default in programming. We know their advantages. I am trying to say there is a world out there that doesn’t look like ours and it works just fine and has other advantages. Are you saying the only real way to program is with generic data structures? I’m saying no because nobody did prior to the late 80s. > I am definitely in favor of reusing an implementation unless you have an excellent reason Let me try one more time. If you examine a program you won’t find 10 different red black trees. Data tends to follow a particular path and there is probably 1 red black tree and it’s a core feature of the application. If that’s true then it’s not a big deal to write that core feature of your application. It avoids premature optimization and code bloat because you tend to use complex data structures when they have a need. Array is a good default. It’s how computer memory works, not just what happens to be lying around.
- gpderetta 1y agoIf you can only write programs by banging rocks together, you will only produce programs that can be written by banging rocks together.
- monkeyelite 1y agoAnd if you're used to writing programs with Dictionary<String, Dictionary<String, Array<String>>> you will do that too.
- gpderetta 1y agoThe irony is that arrays in C are in fact generic. As that is the simplest generic solution that doesn't require boiling the ocean first, that's what many programmers reach for.
- deleted 1y ago[deleted]
- monkeyelite 1y agoHow do I write a C function which operates on array of type T? Which generic operations does the language provide?
- gpderetta 1y agoArrays are generic, but of course C doen't provide generic functions. You can generically copy arrays [1], index them, iterate them, compute their size. But that's not the point, the point is that you can instantiate them for any (complete) T. [1] well, C is weird, so you first need to wrap them in a struct.
- canyp 1y agoGood share, thanks. My approach so far has been to define macros that generate the container type for a given underlying type, as well as all of the relevant type-safe functions, which end up calling into functions that take void*. But the union trick plus the structural identity does seem to simplify things a bit.
- uecker 1y agoIt is cool trick. I already use in my experimental library though ;-) https://github.com/uecker/noplate/blob/main/src/list.h https://github.com/uecker/noplate/blob/main/src/list.h
- eqvinox 1y agoI guess if anyone might know it might be you—do you see any way of doing this for intrusive data structures, embedding the node struct in the data (and as side effect supporting an object to be on multiple containers) rather than the data in the node like you're doing there?
- uecker 1y agoYou could put the dummy member into the embedded node. But for intrusive data structures you often want them to erase the type so that you write generic algorithms as regular functions. In this case, it makes more sense to have a run-time check do to down casts. I do this with my variadic type which has an intrusive "super" member: https://godbolt.org/z/ofdKe7Pfv https://godbolt.org/z/ofdKe7Pfv The overhead is often completely removed by the compiler.
- eqvinox 1y agoMhm. Putting the dummy member into the embedded node doesn't give a path from the proper object to find the embedded node "mid-struct". run-time checks are the "easy way out". We/I'll stick to macro soup probably, so we have compile-time checks. btw. For ISO C WG14… has anyone suggested adding _Include to the preprocessor, along the lines of _Pragma? It'd really help with doing this kind of really long macros, hiding the clunky "#define, #define, #define, #include" inside a macro…
- uecker 1y agoI don't think anybody has proposed this, at least not recently. There is a proposal for multi-line macros: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3531.txt https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3531.txt
- asplake 1y agoInteresting! I’m working on toy/educational generator of ML-style tagged variants and associated functions in C (for a compiler) and when I’m a bit further along I will see if they’re compatible.
- deleted 1y ago[deleted]
- HexDecOctBin 1y agoThe key idea here seems to be to use function pointer's type to enforce type safety rather than using the data "handle" type (that is often found in implementations inspired by Sean Barrett's strechy_buffers). > One annoying thing about C is that it does not consider these two variables to have the same type C23 solves that too: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3037.pdf https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3037.pdf Supported by latest GCC and Clang, but not by MSVC.
- dhooper 1y agoAuthor here. Not quite. The key idea is about using a union to associate type information with a generic data type. Type casting a function is not the only way to use that type information. I discuss that as well as the C23 changes in the footnotes and the "typeof on old compilers" section.
- wahern 1y agoFWIW, as far back as 2015 my feature check library documents Visual Studio as supporting "__typeof".[1] Note the leading but not trailing underscores. Perhaps I was mistaken, but I usually tested that sort of thing. It's also possible __typeof had slightly different semantics. [1] See https://github.com/wahern/autoguess/blob/b44556e4/config.h.guess#L132 https://github.com/wahern/autoguess/blob/b44556e4/config.h.g... (that's the 2015 revision, but HEAD has the same code).
- dhooper 1y agomsvc 19.39 is the first to support it, which I mention in the article. You can confirm it didn't work up through 19.38 in godbolt [1]. I don't use Visual Studio, so I don't know what version of that first started using msvc 19.39 [1] https://godbolt.org/z/M7zPYdssP https://godbolt.org/z/M7zPYdssP
- wahern 1y agoThis is gonna haunt me. Digging through some old code of mine (circa 2009) I found this bit: #elif _MSC_VER >= 1310 #define typeof(type) __typeof(type) So somehow I had the impression Visual Studio .NET 2003 (7.1)[1] added __typeof. I'm still holding out hope someone will come to my rescue and reply that once upon a time MSVC had __typeof, but removed it. But for now it seems past me is gaslighting present me. [1] See https://learn.microsoft.com/en-us/cpp/overview/compiler-versions?view=msvc-170 https://learn.microsoft.com/en-us/cpp/overview/compiler-vers... for mapping between Visual Studio versions and _MSC_VER. EDIT: Ah ha! It seems Microsoft did support __typeof, but perhaps only for "managed" C++ (aka C++ .NET)? > One thing to watch out for when using the __typeof operator in managed C++ is that __typeof(wchar_t) can return different values depending on the compilation options. Source: https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/february/visual-c-net-tips-and-tricks-to-bolster-your-managed-c-code-in-visual-studio-net https://learn.microsoft.com/en-us/archive/msdn-magazine/2002... (See also https://learn.microsoft.com/en-us/archive/msdn-magazine/2005/july/c-at-work-enum-declarations-template-function-specialization https://learn.microsoft.com/en-us/archive/msdn-magazine/2005...)
- luppy47474 1y ago[flagged]
- luppy47474 1y ago[flagged]
- eqvinox 1y agoInteresting idea with the union and using typeof(). We (I) went with large macros defining wrappers instead, which, I believe, is a necessity with intrusive data structures, or at least I don't immediately see how to do that with unions & typeof. Maybe it's possible...? e.g. hash table wrapper: https://github.com/FRRouting/frr/blob/master/lib/typesafe.h#L823-L971 https://github.com/FRRouting/frr/blob/master/lib/typesafe.h#... (cf. https://docs.frrouting.org/projects/dev-guide/en/latest/lists.html https://docs.frrouting.org/projects/dev-guide/en/latest/list...)
- notnmeyer 1y agopretty sure C is the new Go.
- revskill 1y agoWithout the concurreny part.
- qustrolabe 1y agopretty sure C has to go
- deleted 1y ago[deleted]
- o11c 1y agoFor your level 2 code, `uint64_t data[];` is wrong for types whose alignment is greater than `uint64_t`, and also wasteful for types whose alignment is smaller (for example, under an ilp32 ABI on 64-bit architectures). For your level 3 code, it should be `int main() { List(Foo) foo_list = {NULL};` Note that working around a lack of `typeof` means you can't return anything. Also, your particular workaround allows `const`ness errors since `==` is symmetrical. You can't safely omit `payload` since you need it to know the correct size. Consider a `List(int64_t)` and you try to add an `int32_t` to it - this should be fine, but you can't `sizeof` the `int32_t`. Your code is actually lacking quite a bit to make this work. ===== There are 2 major limitations to generics in C right now: * Delegating to a vtable (internal or external) is limited in functionality, since structs cannot contain macros, only functions. * Delegating to an external vtable (mandatory to avoid overhead) means that you have to forward-declare all of the types you'll ever use a vtable with. So far the best approach I've found is to declare (but not define) static functions in the same forwarding header I declare the typedefs in; note that GCC and Clang differ in what phase the "undefined static" warning appears in for the case where you don't actually include that particular type's header in a given TU. (think about writing a function that accepts either `struct SizedBuffer {void *p; size_t len;};` or `struct BoundedBuffer {void *begin; void *end;};`, and also const versions thereof - all from different headers).
- EPWN3D 1y agoI would love for `union`s to be federated, that is, a type could declare itself as thought it was part of a union with another type, without having to pre-declare all possible types in one place.
- gritzko 1y agoHi. I object. The trick#0 you mention is how I made an entire C dialect. Here is a generic binary heap, for example https://github.com/gritzko/librdx/blob/master/abc/HEAPx.h https://github.com/gritzko/librdx/blob/master/abc/HEAPx.h The syntax is a bit heavyweight, but a huge huge advantage is: you get regular C structs in the end, very plain, very predictable, very optimizable. Compiler would eat them like donuts. In the other cases, it is void* and runtime memory sizing and you have to define macros anyway.
- variadix 1y agoI agree, there are actually several reasons to prefer the header impl. Debugging is better, both because you can step through the header code where you can’t with a macro function, and because the type information available to the debugger is better. There are more opportunities for compiler optimizations because each instantiation is monomorphized and you don’t pay a runtime cost with variable sizing, generic structures can also be placed on the stack because of the fixed sizing. There are workarounds for at least two of the problems the author mentions. Naming can be changed from Bar_func(args…) to func(Bar)(args…) with a function name macro that just does name mangling. You can avoid some of the binary bloat by using weak symbols, letting the linker deduplicate functions shared between translation units at link time. There are other problems for generic containers of pointer types however, you can work around them by using a typedef or a type alias. Intrusive data structures are more convenient in C still, but working with them in a debugger is a pain.
- dhooper 1y agoAuthor here. It's worth noting that no work is being done in the macros of my article, they compile down to a normal c function call which you can step through in a debugger. There is little benefit in monomorphizing the implementation of a data structure like a linked list where its behavior doesn't depend on the contents of the data it contains (compared to, say, a max heap)
- dhooper 1y agoAuthor here. Binary heaps and linked lists are different use cases. A binary heap must read the data you put in it to store it correctly, but a linked list doesn't. If I were writing a generic binary heap, maybe I would weigh my options differently. I mentioned this in the footnotes.
- b0a04gl 1y ago[dead]
- ape4 1y agoOr write in CFront and have it translated to C
- layer8 1y agoThe casting of the function type assumes that the item pointer type (e.g. Foo*) has the same representation as void*, which the C standard doesn’t guarantee (in standardese: the two types aren’t “compatible”). Calling the function with the converted type therefore constitutes undefined behavior. It also impacts aliasing analysis by compilers (see [0], incidentally), even if the pointer representation happens to be the same. This casting of the functions to different argument types constitutes the core of the type safety of the generic invocations; I’m not sure it can be fixed. [0] https://news.ycombinator.com/item?id=44421185 https://news.ycombinator.com/item?id=44421185
- dhooper 1y agoThis is addressed in the footnotes. casting is not the core of the type safety. Read the whole article.
- layer8 1y agoAh, that’s what I get for not reading the footnotes. However, the alternative solution presented evaluates the item argument twice, which is problematic as well (but could probably be worked around by passing `(list)->payload` on instead). Secondly, the assignment for type-checking doesn’t work for read-only operations on a const List, or does it? And doesn’t the assignment overwrite the head? Lastly, the do-while construction means you can’t use it for operations that return a value (without compiler extensions). I also don’t agree it’s “squeamish” to be wary of aliasing analysis going wrong. It’s not a clean abstraction and can hide subtle bugs down the road.
- el_pollo_diablo 1y agoSure, but your alternative code incorrectly assigns to (list)->payload. You have many other options. Without typeof, you can if(0) the assignment, or check type compatibility with a ternary operator like 1 ? (item) : (list)->payload and pass that to _list_prepend, etc. With typeof, you can store item in a temporary variable with the same type as (list)->payload, or build a compound literal (typeof(*(list))){.payload=(item)}, etc.
- hgs3 1y agoI'm curious what a hashmap looks like with this approach. It's one thing to pass through or hold onto a generic value, but another to perform operations on it. Think computing the hash value or comparing equality of generic keys in a generic hashmap.
- lhearachel 1y agoI first would question what a user wants to do with a hashmap that uses polymorphic key-values of unknowable type at compile-time. As a thought experiment, you could certainly have users define their own hash and equality functions and attach them to the table-entries themselves. On first thought, that sounds like it would be rife with memory safety issues. At the end of the day, it is all just bytes. You could simply say that you will only key based on raw memory sequences.
- KerrAvon 1y ago> I first would question what a user wants to do with a hashmap that uses polymorphic key-values of unknowable type at compile-time. All of Apple's modern platforms use this concept pervasively, because the Objective-C Foundation framework has a common primitive data structure for it (NSDictionary).
- mfuzzey 1y agoThere's also the method used in the Linux kernel to embed the list information (struct list_head) within the type specific struct. https://kernelnewbies.org/FAQ/LinkedLists https://kernelnewbies.org/FAQ/LinkedLists
- nixpulvis 1y agoThe naming of LIST_HEAD_INIT and INIT_LIST_HEAD is confusing to me.
- mfuzzey 1y agoThe way I remember it is: INIT_LIST_HEAD is of form VERB_NOUN so is called from within a function to programatically initialise the list. LIST_HEAD_INIT is NOUN_VERB and is used within a structure initialiser not from a function. But my main point was to show the "embed the list in the data" approach rather than "embed the data in the list" or "point to the data from the list" and not to discuss the naming details in the kernel implementation of the concept.
- el_pollo_diablo 1y agoNot to mention that they insist on calling every entry of the list a "list head", which makes no sense (hysterical raisins, maybe?). The structure is made of a uniform loop of entries, one of which is used as the actual head & tail, or entry point into the structure.
- RustyRussell 1y agoYes, it's terrible, and the fact that their list_add takes parameters backwards from what one might expect, with no types to catch mistakes! See https://github.com/rustyrussell/ccan/blob/master/ccan/list/_info https://github.com/rustyrussell/ccan/blob/master/ccan/list/_...
- el_pollo_diablo 1y agoAbsolutely. Wrapping the distinguished entry point in a new structure type equipped with a thin type-safe wrapper API that covers the most common use case is the way to go.
- cherryteastain 1y agoWhy would you jump through all these hoops instead of just writing C++ if you want "C with generics"
- Kranar 1y agoBecause for many of the use cases where C is used, switching to C++ involves jumping through even more hoops.
- lionkor 1y agoDo you have a couple of real world examples?
- WalterBright 1y agoHere's how to do it in D: struct ListNode(T) { ListNode* next; T data; } T!int node; Why suffer the C preprocessor? Using preprocessor macros is like using a hammer for finish carpentry, rather than a nail gun. A nail gun is 10x faster, drives the nail perfectly every time, and no half moon dents in your work.
- dhooper 1y agoThanks, this post is about C. On some projects you must use C.
- WalterBright 1y agoIf I may may be provocative :-) this post isn't about C. It's about layering on a custom language using C preprocessor macros. My compilers were originally written in C. I started using the C preprocessor to do metaprogramming. After some years I got fed up with it and removed nearly all of the preprocessor use, and never looked back. My code was much easier to understand. An amusing story: long ago, a friend of mine working for Microsoft was told by a team leader that a 50K program had a bug in it, and sadly the developer was long gone. He'd assigned programmer after programmer to it, who could not fix it. My friend said he'd give it a try, and had it fixed in 2 hours. The source code was written in Microsoft MASM, where the M stood for "Macro". You can guess where this is going. The developer had invented his own high level language using the macro system (which was much more powerful than C's). Unfortunately, he neglected to document it, and the other programmers spent weeks studying it and could not figure it out. The leader, astonished, asked him how he figured it out in 2 hours? My friend said simple. He assembled it to object code, then disassembled the object code with obj2asm (a disassembler I wrote that converts object code back to source code). He then immediately found and fixed the bug, and checked in the "new" source code which was the disassembled version. I've seen many very smart and clever uses of the C macros, the article is one of them. But isn't it time to move on?
- ryao 1y agoIf the C compiler accepts it, it is C.
- deleted 1y ago[deleted]
- ryao 1y agouint64_t data[] in level 2 violates the strict aliasing rule. Use the char type instead to avoid the violation.
- david2ndaccount 1y agoThe “typeof on old compilers” section contains the code: (list)->payload = (item); /* just for type checking */\ That is not a no-op. That is overwriting the list head with your (item). Did you mean to wrap it in an `if(0)`?
- josephg 1y agoIn that example they also had replace the union with a struct - presumably to work around this issue. But that seems wasteful to me too. Doing it within an if(0) seems strictly better.
- JacksonAllan 1y agoI think the idea of using a union to store the element type without any extra run-time memory cost might have some use, specifically in cases where the container struct wouldn't typically store a variable of the element type (or, more likely, a pointer to the element's type) but we want to slip that type information into the struct anyway. However, the problem that I have with this idea as a general solution for generics is that it doesn't seem to solve any of the problems posed by the most similar alternative: just having a macro that defines a struct. The example shown in the article: #define List(type) union { \ ListNode *head; \ type *payload; \ } could just as easily be: #define List(type) struct { \ type *head; \ /* Other data, such as node/element count... */ \ } (As long as our nodes are maximally aligned - which they will be if they're dynamically allocated - it doesn't matter whether the pointer we store to the list head is ListNode *, type *, void *, or any other regular pointer type.) The union approach has the same drawback as the struct approach: untagged unions are not compatible with each other, so we have to typedef the container in advance in order to pass in and out of functions (as noted in the article). This is broadly similar to the drawback from which the "generic headers" approach (which I usually call the "pseudo-template" approach) suffers, namely the need for boilerplate from the user. However, the generic-headers/pseudo-template approach is guaranteed to generate the most optimized code thanks to function specialization[1], and it can be combined with another technique to provide a non-type-prefixed API, as I discuss here[2] and demonstrate in practice here[3]. I'd also like to point to my own approach to generics[4] that is similar to the one described here in that it hides extra type information in the container handle's type - information that is later extracted by the API macros and passed into the relevant functions. My approach is different in that rather than exploiting unions, it exploits functions pointers' ability to hold multiple types (i.e. the return type and argument types) in one pointer. Because function pointers are "normal" C types, this approach doesn't suffer from the aforementioned typedef/boilerplate problem (and it allows for API macros that are agnostic to both element type/s and container type). However, the cost is that the code inside the library becomes rather complex, so I usually recommend the generic-headers/pseudo-template approach as the one that most people ought to take when implementing their own generic containers. [1] https://gist.github.com/attractivechaos/6815764c213f38802227e0db5f692b0d https://gist.github.com/attractivechaos/6815764c213f38802227... [2] https://github.com/JacksonAllan/CC/blob/main/articles/Better_C_Generics_Part_1_The_Extendible_Generic.md https://github.com/JacksonAllan/CC/blob/main/articles/Better... [3] https://github.com/JacksonAllan/Verstable https://github.com/JacksonAllan/Verstable [4] https://github.com/JacksonAllan/CC https://github.com/JacksonAllan/CC
- ValveFan6969 1y ago[dead]
- tehnub 1y agoWhen I saw the title I assumed it was originally "Why I" or "How I" and was trimmed automatically by HN, but this is the original. Could it be that the author was influenced by HN's title guidelines and titled it thus?
- adamnemecek 1y agoDude the ship has sailed.
- dhooper 1y agoNot sure what you mean by that, but if you're trying to imply that C is not relevant: https://www.tiobe.com/tiobe-index/ https://www.tiobe.com/tiobe-index/ Plus an article about C was at the top of hacker news all day today.
- adamnemecek 1y agoNot C per se, but trying to make C type safe.
- tialaramex 1y agoTIOBE is bullshit. When somebody cites TIOBE what they mean is either "I have no idea what I'm talking about, but here's something authoritative seeming that agrees with me" or worse "I know this is bullshit and I don't care" C is still relevant, but TIOBE numbers are not.
- teo_zero 1y ago> Structurally identical types will be considered the same type in GCC 15 and Clang later in 2025 thanks to a rule change Beware that only tagged unions are considered the same type under the new rule, provided they have the same structrure and the same tag. The List(T) macro should be changed to generate a different tag for each different T. Which is trivial (with ##) for simple one-word types, but impossible for even mildly complex ones like pointers to char (strings). Of course you can force yourself to typedef any type before using it in a List, but it looses much of its versatility. Example: typedef char *str; List(str) my_list_of_str; List(str) tokenize(str input) {...}
- alcover 1y ago> Beware that only tagged unions are considered the same type I don't get it. Tagged union is just a design pattern.
- wahern 1y agoBy tagged they mean have an identifier. Compare > struct { ... } foo; and > struct bar { ... } foo; The latter has an identifier, bar; the former doesn't. The standard uses tag to refer to the identifier name, if any, in an enum, struct, or union declaration.
- teo_zero 1y agoExactly, thank you. I've always called "tag" the id that optionally follows struct/union/enum. Is it the wrong word? Some specs call it "name", but "unnamed union" sounds dangerously similar to "anonymous union", which is a different concept, namely (no pun!) an unnamed member of an outer struct or union whose submembers can be accessed as if they belong in the outer one. E.g. struct { struct { int m; }; // no name: anonymous struct s; s.m = 1;
- wahern 1y agoYep, tag is the correct terminology. See, e.g., section 6.7.3.4 Tags in N3220 (https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf)
- pjmlp 1y agoWelcome to around 1992, when Borland published BIDS 1.0 using similar tricks, as C++ templates were yet to be made part of the language. With similar articles in The C/C++ Users Journal and Dr Dobbs, throught their existence. While the effort is commendable, maybe pick a safer language that actually has all the features for type safe generic code.
- kahlonel 1y agoAwesome write up! Please don’t mind the hostile crowd here. They don’t like anything other than their favorite language on the front page. There’s a much bigger silent majority that still loves C and the things you can do with it.
- germandiago 1y agoIf I have to do that I'd rather use C++ templates directly.
- SuperV1234 1y agoLevel 4: switching to C++
- makz 1y agoInteresting but too complicated for me. For my own hashmap implementation I followed a wasteful aproach since I’m. It targeting embedded. I created a structure called a hashmap object. It has two elements: a void pointer and a char pointer. The first one is the data and the second one is the metadata. The metadata is basically a string were the user can put anything, the type of the data, more data, whatever. Then I preallocate 10s of thousands of hashmap objects. That way users of my hashmap don’t have to think about aollocating and de allocating hashmap nodes, they just insert, delete and search freely. They still have to care about allocating and de allocating they’re own data though.
- chuckadams 1y agoI believe "type witness" is the generic (hah) term for "member that doesn't do anything but hold the type". Lot less literature out there about type witnesses than I had thought though...
- gizmo686 1y agoThere's a similar term "phantom type", for when you have some sort of type variable that is never used as the type of an actual variable. I've mostly seen in done in Haskell, but have used it in it Scala as well to simulate type hierarchies not present in the actual type system. In a way, this union trick is kind of like a phantom type, since the secondary type is never actually used.
- chuckadams 1y agoYah, I tend to think of type witnesses as actually existing at runtime and phantom types not, but in the union trick, they don't really exist either. So thinking on it some more, seems more of a way of expressing type parameters in the first place, and well, that's what the article was about. Now I'm wondering what phantom types would look like in C... 8-/
- lakjwerlkawjrl 1y ago"I write type-safe generic data structures in C" sounds like "I build life-size houses out of toothpicks" I can do that to, but I have no desire to. It's so much easier to just switch to Rust.
- jayde2767 1y agoThis is all well and good but, for my mileage, nothing can ever beat C++ generics and the RAII pattern when there is a choice between plain old C and C++ . One second while I get my fire-retardant suit on...