2

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; 
}
Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105
Albert
  • 23
  • 1
  • 5
  • 1
    You'd need to cast to a `char*` for the arithmetic to be valid, but that is then equivalent to a `memset` – UnholySheep Jan 08 '19 at 22:23
  • But, if I cast it to a pointer to a char, will it work everywhere? Is that solution compiler dependent? Is it the best solution? – Albert Jan 08 '19 at 22:29
  • 1
    You should pass the length in bytes, otherwise it is meaningless. – Osiris Jan 08 '19 at 22:30
  • 1
    Look at the standard C [`bsearch()`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/bsearch.html) and [`qsort()`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/qsort.html) functions. Both operate on arbitrary sized arrays of arbitrary types. Use them to guide your thinking. Using a `void *` is part of it; you'll need the number of elements in the array and the size of each element, plus one or more pointers to functions to operate on the data. Beware that GCC allows you to do things with `void *` that the C standard does not. – Jonathan Leffler Jan 08 '19 at 23:01
  • Your example is not very interesting since filling with zero is easily solved regardless of data type (and can be done using the standard function `memset()`). I suggest you modify the example to make it more interesting - i.e. cannot be solved with `memset()`). – Clifford Jan 08 '19 at 23:06
  • You are right - casting to `char*` is not a solution - all bytes will have the value of the least significant byte of the data parameter `data`. – Clifford Jan 08 '19 at 23:09
  • @Clifford, actually, that's just a function, my aim isn't to fill the array with data, 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. – Albert Jan 08 '19 at 23:36
  • You can't pass a parameter of unknown type without passing the size (or something that allows you to calculate the size) as well. `array_fill(array, sizeof array, 0)` (as I imagine it is supposed to work) is just `memset(array, 0, sizeof array)`, except `memset` is highly optimised and more clear to someone who reads your code. – Neil Jan 08 '19 at 23:41
  • @AlbertWilliams : I appreciate that; you have missed my point - it is better to use an example that actually _needs_ the solution and demonstrates its generality. i.e. I am not saying you can just use `memset()`; rather recommending that you change the example to something that cannot be implemented using `memset()` to better illustrate your requirements. – Clifford Jan 09 '19 at 00:01
  • You have edited the question, and I suspect it now generates additional errors for the dereferencing of `*data`. Changing your question in such a way that it invalidates existing otherwise valid answers might not win you friends. I shall not rework my answer, but it no longer matches your code since you changed the signatiure of `fill_array()`. I suggest you roll it back - the change serves little purpose I think. – Clifford Jan 09 '19 at 00:07
  • I rolled it back, I'm sorry if I did but, won't it give an extra error? I mean, I'm assigning an int to a variable whose type I don't know, and I think I should cast it to the type in order to avoid errors, am I wrong? – Albert Jan 09 '19 at 00:15
  • All I am saying is that the code you post should be the code that generated the errors you posted. If you change the code you should also update the error log. But more importantly your changes make some parts of some existing answers plain wrong - you mar making a moving target. Changing the code served no purpose because both versions will fail, and both versions illustrate your problem. – Clifford Jan 09 '19 at 00:44

5 Answers5

5

Pointers point to the beginning of some object in memory. Most pointers also know the size of that object in memory through the type, the exception being void *.

E.g. If the value of a pointer to 32-bit integer is 0, we know bits 0 through 31 contain the data corresponding to that 32-bit integer.

0  31
|---| <- 32 bits storing the data for a 32-bit integer

More importantly for your question, if we know that this pointer points to a sequence of 32-bit integers, we know that we can get the next integer by moving the pointer forward by 32 bits. E.g. the second integer would start at 32.

0  31 32 63
|---| |---|

This is what int[2]. might look like in memory on a 32-bit system

This is how pointer arithmetic works. With a void pointer void *array you cannot do array++ or even *array because there is no way to know how many bits to advance the pointer or how many bits correspond to array.

0    ??
|----

We don't know how many bits a void pointer points to

You can technically get around this by passing the size of the object as well, although this is probably not a good idea.

// array points to the memory to be filled
// len is the number of elements in the array
// size is the size of an element (in bytes)
// fill points to an object to be used to fill array
void array_fill(void* array, int len, size_t size, void* fill) {
    // char is always a single byte
    char* byte_ptr = (char*) array;

    for (int i = 0; i < len; i++) {
        // Fill the current element
        memcpy(byte_ptr, fill, size);

        // Advance byte_ptr the correct number of bytes
        byte_ptr += size;
    }
}

If you don't want to use memcpy you could also manually copy the fill object to byte_ptr one byte at a time.

Increasingly Idiotic
  • 5,700
  • 5
  • 35
  • 73
  • 2
    There is no need to cast to `(void*)` in C. `void *` is assignable from any pointer type (qualifications aside) – Antti Haapala -- Слава Україні Jan 08 '19 at 23:22
  • The pointer does not know the size of what it points to, only where it starts. The compiler knew the size, but by the time the cide executes the compiler is long-gone. – rici Jan 09 '19 at 03:13
  • @rici the same argument could be made that the "pointer" does not exist at execution. Only some offset into memory. – Increasingly Idiotic Sep 17 '21 at 21:06
  • @IncreasinglyIdiotic: I agree. If you had said that the pointer knows it is a pointer, I would have made the same remark. In fact, the pointer doesn't even know that it's an offset into memory (except on architectures like the long-defunct LISP machine), just like a `double` doesn't know whether it [represents pounds or newton-seconds](https://en.wikipedia.org/wiki/Mars_Climate_Orbiter#Cause_of_failure). – rici Sep 18 '21 at 00:29
2

The problem here is two-fold. First is dereferencing the void pointer and the other is doing arithmetic with it. The compiler also warns you about it as you've shown in your post.

You can't directly add to a void pointer since the compiler doesn't know how much further it needs to go when adding addresses, it needs the size of the thing being pointed to, to be able to do it. Therefore, you'd need to typecast the void to something concrete before you can add stuff to it.

Similarly, you cannot dereference a void pointer because again the compiler doesn't know how many bytes to extract since void doesn't have any implicit length.

Kartik Anand
  • 4,513
  • 5
  • 41
  • 72
  • That explains why he is getting the error, but does not answer the question : _"Is there a way...?"_ – Clifford Jan 08 '19 at 23:10
2

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.

Clifford
  • 88,407
  • 13
  • 85
  • 165
  • Oh, thank you! I'm getting it better, actually it's an interesting matter and I want to know more about it, where can I find more information? What books do you recommend? – Albert Jan 09 '19 at 00:10
  • @AlbertWilliams : More about what in particular. IMO good books on C stopped being written when it stopped being useful - it is a small, language there is not much more you can say about it beyond a certain point. The minor additions to the language since its first standardisation in ANSI C89 / ISO C90 don't take much explaining, Understanding C is helped by understanding the fundamentals of computer architecture at a relative low level because it exposes that architecture, so a more general reading of computing fundamental is in order perhaps. – Clifford Jan 09 '19 at 00:33
  • If I had to call it, despite not being updated to later ISO C standards, I'd still recommend the seminal K&R C Programming Language. It will teach you the fundamentals of C, however not necessarily how to program or construct software in a broader sense. – Clifford Jan 09 '19 at 00:36
1

Too much like @Increasingly Idiotic good answer.

So I'll make this wiki. Useful as a reference.


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

Yes code can call such a function with an array, yet the array will be converted to the address of the first element of the array. It is that address the function will use.

some_type a[N];
foo(a);

To make the function accept any array object type, the function parameter is void *.

int foo(void *address_of_first_element);

Unfortunately foo() has lost the type.


Is there any way to achieve it?

In OP's case, array_fill() only needs the size of the type and not the type itself. So pass in the sizeof the type.

OP sees the array size is needed and passes it - good. What is also needed is the size of the element and a pointer to the fill value.

To do pointer math, convert the void* to char * as pointer math on void* is not defined by C.

// int array_fill(void* array, int size, int data)
int array_fill(void* array, size_t a_size, const char *fill, size_t e_size) {
  char *data = array;
  for(size_t a = 0; a < a_size; a++) {
    memcpy(data, fill, e_size);  // Copy `e_size` bytes.
    data += e_size;              // Advance `e_size` bytes. 
  }
  return 0; 
}

int main(void) {
  int array[ARBITRARY_SIZE], fill_value = 42;    
  array_fill(array, array_length(array), &fill_value, sizeof array[0]);

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

  return 0;
} 
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
0

If you expect to fill an array of type with data, let's say an array of double with the value 2.2 or even en array of structure { int a; anything b }; then basicaly the answer is no, you can't do this this way.

You could use a macro for this, like

# define FILL_ARRAY(arr, data, len) for (size_t i = 0; i < len; i++) { arr[i] = data }

But it is not a function.

But you could create a function that takes a callback able to assign a data, like :

void fill_array(void * array, size_t item_size, size_t array_len, void (*cb)(void *))
{
    unsigned char *bytes = array;
    for (size_t i = 0; i < array_len; i++) {
        cb(&bytes[i * item_size]);
    }
}

void fill_double(void *data)
{
    const value = 2.2;
    double *ptr = *data;

    *data = value;
}

int main(void)
{
    double array[30];

    fill_array(array, sizeof double, 30, fill_double);
}

Not sure that this is worth, but it should look like a solution to your question ( not compiled, may contain errors )

mmeisson
  • 623
  • 6
  • 22