r/C_Programming • u/Object_71 • 7d ago
Writing generic code in C – Part 2
https://thatonegamedev.com/cpp/writing-generic-code-in-c-part-2/After some comments on my previous post about writing generic code in C where people argue that this is “poor man’s overloading” I wanted to add a new technique that allows you to write real generic style code in C with the only drawback. You could even combine the technique from this lesson and the […]
31
u/SmokeMuch7356 7d ago
Hot take incoming...
Generic programming in C is a fool's errand. Yes, you can do it, I've done it. It's a shitload of work, it's easy to get wrong, it will never be 100% type safe, it requires awkward programming styles (the alternating #defines and #includes make me itch), on and on and on and on. _Generic helps, but not that much.
For my part I take cues from qsort and bsearch - treat everything as a void *, use type-aware callbacks for comparisons, assignment, output or formatting, etc. Unfortunately that throws type safety out the window and into oncoming traffic (and you still have to create type-aware front ends if you want to be able to handle literal values). IMO it's miles less confusing than macro-based approaches, but it's still a half-assed solution.
There's just no good way to do it. It's more of a pain in the ass than it's worth. If you absolutely need true generic support, use C++, or Java, or a language with the capability built into it. C at best gives you a crayon drawing that can look a little like generic support.
6
u/pjl1967 7d ago
For my part I take cues from
qsortandbsearch- treat everything as avoid *, use type-aware callbacks for comparisons, assignment, output or formatting, etc.That's the right direction for C, but you can add type information after the fact; see here. (You can scroll down to Adding Type Information.)
1
u/Object_71 7d ago
This is a cool way to add type information for type erased arrays as well. My example in the article uses an array as I thought that it would seem most recognizable as a pain point for C programmers but it is a generally good way to reuse code in a templated way. I actually first decided to use it in a software renderer project where I abstracted 95% of the code for rasterization and the templated part was just some differentiation between textured rendering or colored where they had small optimizations.
2
u/Object_71 7d ago
Treating everything as void* comes with a bit of a cost and is something which in this case would be better performing in C++ due to the type erasure. I am a fan of C and some projects can still be written in C (embedded for example) and you can benefit from the knowledge on how to easily produce repeatable code without actually erasing types or holding addional fields of data for element size. The method I am proposing is also mostly supported by an IDE (in terms of intellisense magic) and is supported in stack traces and debug breaking.
1
u/orbiteapot 7d ago
Treating everything as void* comes with a bit of a cost and is something which in this case would be better performing in C++ due to the type erasure.
void*-based generics is a very well known pattern for C compilers. They will get rid of either object or function indirection, as long as you provide them with some guaranteed (e.g., that the implementation is always visible in a given TU, FPs arestatic const, etc.). So, it will be on par with C++ on performance, just not on ergonomics/type safety.1
1
u/c_a1eb 6d ago
from an embedded perspective, in my experience you almost never really need generics, in most cases it indicates that you should rethink your architecture and either find a different approach or more commonly have a container object to store the type information (e.g as a union). This absolutely can lead to code bloat so imo you need to really be able to justify it. The alternative is to acknowledge and understand common patterns for explicit cast and just deal with it, or introduce static inline functions to handle the cast explicitly if there is still doubt (a smart move there is to make the inner type private so the API consumer only ever gets opaque pointers and has to make explicit calls to access their data).
that being said, i think that trying to implement generics and learning stuff like X macros and other preprocessor internals is a really good way to develop your understanding of C, getting a good grasp of common helpers like the common container_of() (which imo is the backbone of how the kernel does generics) absolutely makes you a better C programmer.
1
u/readmodifywrite 7d ago
It also hamstrings the optimizer. Knowing exactly what types we're dealing with is one of the ways the compiler can generate efficient machine code.
Personally I've never encountered a case where I needed generics in 25 years of C programming.
1
u/aalmkainzi 1d ago
I've used a library named STC, which provide generic containers. Works pretty well
3
u/pjl1967 7d ago
Many make the mistake of trying to put the type into the container using macros for the container itself. Among other things, that way leads to code bloat. There is another way as described here. (You can scroll down to Adding Type Information.)
2
u/WittyStick 7d ago
The "code bloat" is monomorphization, which can have big improvements on performance. It's what C++ does when you use templates.
Using
void*works well for many cases - but it doesn't work when you want internal storage of non-pointers - eg, an array ofint. An array of pointers tointis much worse on performance.We can use
intptr_tas the type of ourdata, which then lets us use it as either a pointer or an integer, and decide which based on context - but this is still limiting. What if we want floats, or some other custom data structure, without requiring pointers to it?1
u/pjl1967 7d ago
Using
void*works well for many cases - but it doesn't work when you want internal storage of non-pointers - eg, an array ofint. An array of pointers tointis much worse on performance.True, but you're conflating node-like data structures (like trees) with contiguous data structures (like arrays). You don't have an array of pointers to
int; you have one pointer to an array ofints.(Did you even read the link-to article that explains the dynamic array implementation using exactly one pointer to an arbitrary number of elements of any size?)
For node-like data structures, you can use flexible array members as shown here.
1
u/Object_71 7d ago
Actually having the type information the way I show in my article allows for something which I will soon also write about and it is adding GDB extensions with the array type. You could then convert the dynamic array with unknown size to a known size array for GDB or LLDB and actually inspect the values that are filled in this dynamic array and not test them one by one in the debugger, and frankly way better than having a type erased array.
2
u/chocolatedolphin7 7d ago
There is nothing particularly wrong with what you described and it is exactly what things like C++ STL containers do, just with different syntax.
It's not code bloat, that's a misleading term to use. It's pretty clear and trivial to understand that "separate instances" of code are being generated from macro expansion. Unless you go overboard, this has almost no impact on performance in practice. Mostly just bigger binaries if overused, and the implications of that.
The linked post also contains inaccurate statements like "entire implementation must be in the header." This is not technically true and not the case with how I personally use generic containers in C. Did you use an LLM?
1
u/pjl1967 7d ago
It is code bloat since, for every type T, the code is repeated (exactly the same as it is with templates in C++).
Unless you go overboard, this has almost no impact on performance in practice.
I never said anything about performance.
Mostly just bigger binaries if overused ...
Yes, that's what code bloat means.
The linked post also contains inaccurate statements like "entire implementation must be in the header." This is not technically true and not the case with how I personally use generic containers in C.
Many implementations either require (or have) the entire implementation in the header, especially if they embed the T rather than a
void*that is then later cast toT*.Indeed, your implementation has the entire implementation in the header
array.incwhich is why you needstaticfunctions.Did you use an LLM?
No. Did you?
1
u/chocolatedolphin7 7d ago
Bloat has a very negative connotation, usually with the implication being considerably worse performance metrics like cpu cycles, memory usage, file size, etc.
But in this context the impact on resulting binary size is so negligible it could never be called bloat. And for the record, using void* has quite a few other (imo strictly worse) drawbacks. But this has been discussed many times before elsewhere.
Indeed, your implementation has the entire implementation in the headerarray.incwhich is why you need static functions.What implementation? You didn't see my implementation. I'm not the OOP. But obviously it has been done before and it's not rocket science. There's a few ways you could do it. You don't need to use static if you don't want to, the compiler has no concept of header files and you only need to avoid duplicate definitions.
1
u/Object_71 7d ago
Unlike macro magic the article produces code that is easy to write:
- no additional slashes like in macros
- some IDE support (CLion highlights most of the code but decides only one of the types as the implementation)
- full debugging support - unlike macros these functions are actual functions that appear in stack traces and code is properly stepped over
The only con is around the function or type names in this file where there has to be some name concatenation to produce unique function names.
1
u/pjl1967 7d ago
I still don't see why the conventional technique is better than the anonymous
uniontrick.1
u/Object_71 7d ago
When debugging you would always have the typed type even in the generic sections where it is type erased in hte article you provide. Also no need for the additional size variable for the element size. And the solution I write for in my article can be used for more than arrays but generally for writing generic code. I just gave an example with an array. I actually used it for a few sections of a project where I had repeatable code with small differences of some types where I can share 95% of the code.
1
u/pjl1967 7d ago
I'll grant that debugging becomes a bit simpler, but all debuggers support casting, e.g.,
(int*), when printing values, so it's not like debugging type-erased stuff is impossible. You could probably also extend your favorite debugger to understand theuniontrick and automatically apply the cast.The
esizeis fairly negligible since there's only one per container, not per element.The solution I use for node-like data structures (e.g., trees) is to use flexible array members that can store the node's data internally to the node (if you want).
1
u/Physical_Dare8553 7d ago
string concatonation is not the way for this because of complex types, like const pointers and such. i think it's better to let the user provide a name and a type separatley
i dont use this method for arrays but i have something similar for my hashmaps
`
#define mapconfig dbgallocator_map, void *, struct tracedata, ((iptr)k), ((iptr)a - (iptr)b)
#include "../incmap.h"
`
1
u/theNbomr 5d ago
I cannot envision producing by hand, code that reads like what you're describing, ever.
However... I can easily imagine some kind of C source code generator that is the back end of some kind of tool leveraging it. I'm thinking of GUI producing tools or perhaps code to produce an implementation of network protocols a la ASN.1
There is a good bit of merit to your project in getting a lively discussion going between people who have some advanced knowledge and skills.
2
19
u/Snarwin 7d ago
The problem with using concatenation like this is that it falls apart as soon as you want to use a type like
const char *that contains non-identifier characters. It's possible to work around this with atypedef, but really the correct approach is to let the user#definebothTandTNameseparately.