There are many ways to implement for example containers. None of them are so clean as using templates/generics.
1. One trick is using void pointers in the container implementation and later you store pointers to the items you want to store in the container. Of course you have to do ugly casts with this implementation and you are using pointers unnecessary compared to C++ templates where you can store the items by value without allocations. If you want to store primitive types like char and short you can cast these into pointers and store them directly by value to the container but your are still wasting space this way if the size of the void* is larger than the primitive you want to store.
2. Many things that you are doing with templates in C++ were done with macros in C. I've seen for example dynamic array implementation where you could generate the source code of the dynamic array for a give type by declaring it with a macro:
typedef struct SMyData
{
int data0;
char data1;
float data2;
} SMyData;
DECLARE_DYN_ARRAY_FUNCTIONS(SMyData)
DEFINE_DYN_ARRAY_FUNCTIONS(SMyData)
void DynArray_SMyData_Add_(DynArray_SMyData* arr, const SMyData* item);
void DynArray_SMyData_Remove(DynArray_SMyData* arr, const SMyData* item);
...
Drawbacks: Lots of code in macros is not only ugly bug undebuggable - you can not place breakpoints in macros.
3.
Another nice trick I've seen in C in a linked list implementation:
typedef struct LinkedListNode
{
struct LinkedListNode* next;
} LinkedListNode;
void LinkedList_Append(LinkedListNode** head, LinkedListNode* node);
void LinkedList_Remove(LinkedListNode** head, LinkedListNode* node);
#define NODE_TO_DATA(node_ptr, node_member_name, data_type) \
(data_type*)((char*)node_ptr - offsetof(data_type, node_member_name))
struct SMyNode
{
int mydata;
char mydata2;
float mydata3;
LinkedListNode node;
};
LinkedListNode* g_Head = NULL;
void LinkedListTest()
{
LinkedListNode* it;
SMyNode node0, node1;
LinkedList_Append(&g_Head, &node0.node);
LinkedList_Append(&g_Head, &node1.node);
it = g_Head;
while (it)
{
SMyNode* node = NODE_TO_DATA(it, node, SMyNode);
it = it->next;
}
}
This trick works with any data structures that consist of nodes/items (for example in case of trees) that don't have to be allocated by packing them together like in case of an array. Basically your data contains the container related stuff and not the container contains your data...