September 30, 2019
Dynamic typing makes for clean and flexible programs, but it also means that subtle bugs can hide in places that a strongly typed language like C would find at compilation. You may have wondered if C can mimic these dynamically typed languages to some degree. While that isn’t how the language is designed to be used, there are tricks you can use to make C more generic so that the functions you write don’t appear to be inextricably tied to any specific type.
Void Pointers
The void pointer is how to define an untyped variable in C. It simply holds an address passed to it and must be correctly typecasted before it can be used. Here’s a small program to show how you’d put void pointers into use.
#include <stdio.h>
typedef enum {
TYPE_INT = 0,
TYPE_CHAR,
TYPE_FLOAT
} voidType_t;
void print(voidType_t type, void* arg) {
switch (type) {
case TYPE_INT:
printf("int: %d\n", (int)arg);
break;
case TYPE_CHAR:
printf("string: %s\n", (char*)arg);
break;
case TYPE_FLOAT:
printf("float: %f\n", *(float*)arg);
break;
default:
printf("Unknown type!\n");
break;
}
}
int main(int argc, char** argv) {
int number = 100;
char* string = "hello";
float pi = 3.14;
void* pPi = π
print(TYPE_INT, number);
print(TYPE_FLOAT, pPi);
print(TYPE_CHAR, string);
while (1) {}
return 0;
}
The void pointer has allowed the print function to handle 3 different data types in its second argument. But, because this is still C the function must also accept an argument that defines the type the second argument is following. Tyepcasting the void pointer correctly can also be tricky. The float for instance has to be casted to a pointer to float type since you cannot directly cast a void * to a float in C, and then must be dereferenced to access the contents of the variable. Just keep in mind this method is completely circumventing C’s type system and can lead to undefined behavior!
_GENERIC
Here’s another bit of generic programming in C that was added into C11 and future releases, the _Generic macro. It’s a bit like a switch statement that can parse based on the type being passed into it, allowing you another way to decouple functions from specifically defined types.
#include <stdio.h>
#define format_print(x) _Generic((x), \
signed int: "%d", \
float: "%f", \
double: "%f", \
char *: "%s", \
void *: "%p")
#define print(x) printf(format_print(x), x), printf("\n")
int main(int argc, char** argv) {
int number = 100;
char* string = "hello";
double pi = 3.14;
print(number);
print(pi);
print(string);
return 0;
}
While I don’t have much experience with it, here’s a link to a post that gets into much greater detail:
http://www.robertgamble.net/2012/01/c11-generic-selections.html
As we’ve seen, tools like the ones above can indeed help make C appear less statically typed, but you could also end up unnecessarily obfuscating your code or making it potentially dangerous by casting pointers. Use with caution!