Search code examples
cansi-c

Is there any way to pass an array of an unknown type as a parameter to a function in C?


I've been trying to improve my skills and knowledge in C. Today I've tried to create a function that takes an array of any type, but I haven't found a successful way, I'm using ANSI C and I tried to pass it as a void pointer, but when I'm trying to go through the memory operating with the parameter, the compiler complains. Is there any way to achieve it? I was thinking that possibly it can be done through pre-processor directives, but I ain't sure.

P.S: My aim isn't to fill the array with data, that's just a function, but to understand and learn how to pass data if I don't know its type, or allow my function to work with more than just one type of data.

This is the output of the compilation process:

array_test.c: In function 'array_fill':

array_test.c:34:13: warning: pointer of type 'void *' used in arithmetic [-Wpointer-arith]

*(array + i) = data;

^

array_test.c:34:5: warning: dereferencing 'void *' pointer

*(array + i) = data;

^~~~~~~~~~~~

array_test.c:34:5: error: invalid use of void expression

*(array + i) = data;

^

And this is my code:

#include <stdio.h>

#define array_length(array) (sizeof(array)/sizeof(array[0]))

#define ARBITRARY_SIZE 10    

typedef enum
{
  false,
  true
} bool;

int array_fill(void*, int, int);

int main(int argc, char* argv[])
{
  int array[ARBITRARY_SIZE];
  int i;
 
  array_fill(array, array_length(array), 0);

  for(i = 0; i < array_length(array); i++)
  {
    printf("array[%d]: %d\n", i, *(array + i));
  }

  return 0;
} 

int array_fill(void* array, int size, int data)
{
  int i;
  
  for(i = 0; i < size; i++)
  {
    *(array + i) = data; 
  }

  /*I will implement a check later, in case of errors.*/
  return 0; 
}

Solution

  • Without a type the data referenced by array has not element size so the pointer arithmetic is undefined. For the expression to be valid, you must cast array to an appropriate data type such as:

    *((int*)array + i) = data; 
    

    But that defeats the purpose of having an undefined type. The simple and most efficient solution is to define separate functions for array each type you wish to fill. It is possible to define a function that will handle multiple integer types thus:

    int array_fill(void* array, size_t array_length, long long data, size_t data_size )
    {
        if( data_size > sizeof(data) )
        {
            data_size = sizeof(data) ;
        }
    
        for( size_t i = 0; i < array_length; i++)
        {
            for( int b = 0; b < data_size; b++ )
            {  
                ((char*)array)[i * data_size + b] = (data >> (b * 8)) & 0xff ;
            }
        }
    
      return 0; 
    }
    

    The above makes two assumptions:

    • the target uses is little-endian byte order,
    • the target has an 8 bit char type.

    Modifications are necessary where those assumptions are not true. Note I have also used array index notation rather than pointer arithmetic - it results in fewer parentheses so is easier to read.

    The function might then be called, in your case for example thus:

    array_fill( array, array_length(array), 0, sizeof(*array) ) ;
    

    and array may have any type.

    However filling an array with zero is a special case that does not need this complexity, (i.e. for your example usage it serves no purpose). The following :

    memset( array, sizeof(array), 0 ) ;
    

    has the same effect some all bytes of the integer 0 are zero in any case. The function is of more use for values where each byte differs.

    array_fill( array, array_length(array), 0x01234ABCD, sizeof(*array) ) ;
    

    Now if array is of type uint8_t for example it will be filled with 0xCD, if it is uint16_t then 0xABCD. If it were long long and on the target that is a 64 bit type, it will be filled with 0x0000000001234ABCD.

    It is possible if somewhat cumbersome to also use this function to fill a float or double array thus for example:

    double array[ARBITRARY_SIZE];
    double x = 0.5 ;
    array_fill(array, ARBITRARY_SIZE, *(long long*)(&x), sizeof(array) );
    

    Another approach that allows also aggregate types or even arbitrary length sequences to be used as the fill is:

    int array_fill( void* array, size_t array_length, 
                    const void* fill_pattern, size_t fill_pattern_length )
    {
        for( size_t i = 0; i < array_length; i++)
        {
            for( int b = 0; b < fill_pattern_length; b++ ) 
            {  
                ((char*)array)[i * fill_pattern_length + b] = ((char*)fill_pattern)[b] ;
            }
        }
    
      return 0; 
    }
    

    Then it can be used truly for any type. Examples:

    Double

    double array[ARBITRARY_SIZE], x = 0.5 ;
    array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );
    

    int

    int array[ARBITRARY_SIZE], x = 123456 ;
    array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );
    

    struct

    struct S{ int x; double f ; } array[ARBITRARY_SIZE], x = {1234, 0.5};
    array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );
    

    2D array

    int array[ARBITRARY_SIZE][2], x[2] = { 12, 98 } ;
    array_fill( array, ARBITRARY_SIZE, &x, sizeof(x) );
    

    The implementation avoids endian issues, but cannot accept literal-constant initialisers, because you cannot take the address.

    This last implementation can be improved (simplified, and made more efficient); for example:

    int array_fill( void* array, size_t array_length, 
                    const void* fill_pattern, size_t fill_pattern_length )
    {
        for( size_t i = 0, byte_index = 0; 
             i < array_length; 
             i++, byte_index += fill_pattern_length )
        {
            memcpy( &((char*)array)[byte_index], fill_pattern, fill_pattern_length ) ;
        }
    
      return 0; 
    }
    

    That's the version I'd go with.