7
\$\begingroup\$

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;
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$
  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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.