You can get most of the functionality of C++ templates via the C
preprocessor.
Indeed, I used that trick in "C Unleashed" in chapter 13.
By defining a data type, a new sort routine specific to that data type
is created (types with _P2_ in their name are 'pointer to' types):
ALLSORT.H ( 56): #if defined(E_TYPE_UNSIGNED_INT)
ALLSORT.H ( 58): #elif defined(E_TYPE_SIGNED_INT)
ALLSORT.H ( 60): #elif defined(E_TYPE_UNSIGNED_LONG)
ALLSORT.H ( 62): #elif defined(E_TYPE_SIGNED_LONG)
ALLSORT.H ( 64): #elif defined(E_TYPE_UNSIGNED_CHAR)
ALLSORT.H ( 66): #elif defined(E_TYPE_SIGNED_CHAR)
ALLSORT.H ( 68): #elif defined(E_TYPE_UNSIGNED_LONG_LONG)
ALLSORT.H ( 70): #elif defined(E_TYPE_SIGNED_LONG_LONG)
ALLSORT.H ( 72): #elif defined(E_TYPE_FLOAT)
ALLSORT.H ( 74): #elif defined(E_TYPE_DOUBLE)
ALLSORT.H ( 76): #elif defined(E_TYPE_LONG_DOUBLE)
ALLSORT.H ( 78): #elif defined(E_TYPE_P2_UNSIGNED_INT)
ALLSORT.H ( 80): #elif defined(E_TYPE_P2_SIGNED_INT)
ALLSORT.H ( 82): #elif defined(E_TYPE_P2_UNSIGNED_LONG)
ALLSORT.H ( 84): #elif defined(E_TYPE_P2_SIGNED_LONG)
ALLSORT.H ( 86): #elif defined(E_TYPE_P2_UNSIGNED_CHAR)
ALLSORT.H ( 88): #elif defined(E_TYPE_P2_SIGNED_CHAR)
ALLSORT.H ( 90): #elif defined(E_TYPE_P2_UNSIGNED_LONG_LONG)
ALLSORT.H ( 92): #elif defined(E_TYPE_P2_SIGNED_LONG_LONG)
ALLSORT.H ( 94): #elif defined(E_TYPE_P2_FLOAT)
ALLSORT.H ( 96): #elif defined(E_TYPE_P2_DOUBLE)
ALLSORT.H ( 98): #elif defined(E_TYPE_P2_LONG_DOUBLE)
ALLSORT.H ( 100): #elif defined(E_TYPE_STRING)
ALLSORT.H ( 102): #elif defined(E_TYPE_COLLATED_STRING)
ALLSORT.H ( 107): #endif /* defined(E_TYPE_... */
However, it is a very poor substitute for templates.