C has a different beauty for it than C ++, and is gaining security and can always see that everything happens when tracing through code without involving drops in your debugger is usually not one of them.
C beauty comes from the lack of type security, work around the type system and the raw level of bits and bytes. Because of this, there are some things that it can do more easily without fighting the language, for example, variable-length structures, using the stack even for arrays whose sizes are determined at runtime, etc. It is also, as a rule, much easier to keep the ABI when you work at this lower level.
So, there is another aesthetics, as well as various problems, and I would recommend changing your mindset when you work on C. To really appreciate this, I would suggest doing what many take for granted for example, for example, to implement your own dispenser memory or device driver. When you work at such a low level, you cannot help but look at everything as a mock memory of bits and bytes, unlike "objects" with attached behavior. In addition, in such a low-level bit / byte control code, a point may appear where C becomes easier to understand than C ++ code heaped with reinterpret_casts , for example.
As for the linked list example, I would suggest a non-intrusive version of the linked node (one that does not require storing the list pointers in the element type, T itself, allowing the logic of the linked list and the view to be separated from T itself), for example:
struct ListNode { struct ListNode* prev; struct ListNode* next; MAX_ALIGN char element[1];
Now we can create a node list as follows:
struct ListNode* list_new_node(int element_size) { // Watch out for alignment here. return malloc_max_aligned(sizeof(struct ListNode) + element_size - 1); } // create a list node for 'struct Foo' void foo_init(struct Foo*); struct ListNode* foo_node = list_new_node(sizeof(struct Foo)); foo_init(foo_node->element);
To extract an item from a list as T *:
T* element = list_node->element;
Since this is C, there is no type check when pointing to the pan in this way, and this will probably also make you feel uneasy if you come from C ++ background.
The hard part here is to make sure that this member element correctly aligned for whatever type you want to keep. When you can solve this problem as mobile as possible, you will have a powerful solution for creating efficient memory layouts and allocators. Often this means that you simply use maximum alignment for everything that may seem wasteful, but usually it is not if you use appropriate data structures and allocators that do not pay this overhead for many small items on an individual basis.
Now this solution is still related to type casting. There is little that can be done if you have a separate version of the code for this list node and the corresponding logic for working with it for each type of T that you want to support (with the exception of dynamic polymorphism). However, it does not require an additional level of indirection, as you might think, and still selects the entire node list and element in one distribution.
And I would recommend this easy way to achieve versatility in C in many cases. Just replace T with a buffer whose length matches sizeof(T) and is correctly aligned. If you have a sufficiently portable and safe way that you can generalize to ensure proper alignment, you will have a very powerful way of working with memory, which often improves cache hits, reduces the frequency of heap allocation / deallocation, the amount of required direction, build time, etc. d.
If you need additional automation, for example, list_new_node automatically initialize struct Foo , I would recommend creating a general type table structure that you can pass, which contains information such as big T, a function pointer that points to the function to create a default instance for T , the other for copying T, cloning T, destroying T, comparator, etc. In C ++, you can automatically generate this table using templates and built-in language concepts such as copy constructors and destructors. C requires a bit more manual effort, but you can still slightly reduce its pattern with macros.
Another trick that can be useful if you are following the route of generating more macro-oriented code is to use a prefix or suffix convention for identifier names. For example, CLONE (Type, ptr) can be defined to return Type##Clone(ptr) , so CLONE(Foo, foo) can call FooClone(foo) . This is a kind of cheat to get something like an overload function in C and is useful when generating code in bulk (when CLONE is used to implement another macro) or even a bit of copying and pasting template-type code, at least to improve the uniformity of the template.