C function to print arrays of arbitrary data type

C is naturally clumsy at polymorphism but I wanted to avoid duplication so I wrote this function. But I'm sure there are ways left to improve it. Please not that for brevity's sake not all data types have been included.

void print_int(const void *number) {
int result = *((int*) number);
printf("%d ", result);
}

void print_double(const void *number) {
double result = *((double*)number);
printf("%f ", result);
}

/// Prints arrays of arbitrary type
/// \param array The array to print
/// \param length The length of the array
/// \param type The data type xxx of the array elements in the form of "TYPE_XXX"
/// \return 1 in case of an invalid data type
int print_array(const void *array, size_t length, char *type) {

void (*function)(const void *number);
void *cast_array;
size_t size;

if (!strcmp(type, TYPE_INT)) {
function = print_int;
cast_array = (int *) array;
size = sizeof(int);
} else if (!strcmp(type, TYPE_DOUBLE)) {
function = print_double;
cast_array = (double *) array;
size = sizeof(double);
} else {
return 1;
}

for (size_t i = 0; i < length; ++i) {
function(cast_array + i * size);
}
printf("\n");
return 0;
}


First of all this is a good attempt to provide polymorphism for print functionality. Few comments that might help:

I studied the print_array function and we can certainly do few improvements to it.

1) int print_array(const void *array, size_t length, char *type)

The type parameter is defined as char *. Since this function will support only a predefined types its better to have those as enum values. for e.g. you could do a typedef as

typedef enum{
INT = 0,
DOUBLE,

MAX_TYPES,
}SUPPORTED_TYPES;


2) if (!strcmp(type, TYPE_INT)) We can avoid these comparisons by making use of a Map like feature. Your current approach requires changing print_array function for any new type that we add. For e.g. tomorrow if you add support for print_char you will have to make those changes in print_array as well. Wouldn't it be better if print_array required no change. Ok so how to do it. In C++ there is a rule to replace conditional statements with polymorphism or maps. We will use maps here.

Since we already changed the type to an enum we can define a new struct which holds what functions to call for what data-types and the size of data-type as below:

typedef struct{
void (*function)(const void *number);
int size_of_elements;
}FunctionMap;


Now we create a mapping for data-types to function and store it in a global variable say print_map

const FunctionMap print_map[] = {
[INT] = {print_int,sizeof(int)},
[DOUBLE] = {print_double,sizeof(double)},
};


Now your print_array function changes just to few lines i.e. a lookup into this print_map for the type that is passed.

int print_array(const void *array, size_t length, SUPPORTED_TYPES type)
{

assert(type<MAX_SUPPORTED);
assert((sizeof(print_map)/sizeof(FunctionMap)) == MAX_SUPPORTED);

void (*function)(const void *number);
const void *cast_array = array;
size_t size;
size_t i;
function = print_map[type].function;
size = print_map[type].size_of_elements;
for (i = 0; i < length; ++i) {
function(cast_array + i * size);
}
printf("\n");
return 0;
}


Note that I have added few asserts to make sure that the user passes proper values for type and that the implementer has add all data-type mappings in print_map. Now whenever you define a new print type say print_char you don't need to change print_array. Just add that function and its mapping in print_map and everything should work as it is.

Hope this helps.

1. Code falls short of "arrays of arbitrary data type" as it uses a void *array. Portably, a void * works for a pointer to an object, but not necessarily a pointer to a function. See C11 6.3.2.3 Pointers for details. C does not have a simple universal pointer type.

2. Invalid pointer arithmetic. Although a number of compilers will perform pointer math on a void *, some as if 'twas a char *, it is still undefined behavior.

// void *cast_array;
// function(cast_array + i * size);
char *cast_char_array = array;
function(cast_char_array + i * size);

3. Printing a double with "%f" will print about half of all possible double as 0.000000 or -0.000000owing to the many values smaller than 0.0000005. It will also display large values with many digits, perhaps 100s. Recommend using "%.*e" with sufficient precision. Details

void print_double(const void *number) {
double result = *((const double*)number);
printf("%.*e ", DBL_DECIMAL_DIG - 1, result);
}

4. Minor: not a fan of ending a line with " " and then the "\n". Prefer just the "\n".

const char *sep = "";
for (size_t i = 0; i < length; ++i) {
fputs(sep, stdout);
function(state_variable, cast_char_array + i * size, i);
sep = " ";
}
fputs("\n", stdout);

5. Expansion: Passing in the separator, or a state variable, as an argument like " ", or ", ", etc. would be useful. The return value of function() could be used to break the loop.

int retval = 0;
for (size_t i = 0; reval == 0 && i < length; ++i) {
revtal = function(state_variable, cast_char_array + i * size, i);
}
...
return retval;


You may find Formatted print without the need to specify type matching specifiers using _Generic interesting.