5
\$\begingroup\$

I've been wondering for some time what the best way to pass a 2d array to functions is. I know that arrays, passed to functions, decay into pointers, so int arr[25] would become int *arr inside a function. The problem is with 2d arrays, int arr[5][5] would not become int **arr. Instead I've been taught to pass it like this: int (*arr)[5]. The problem with that is that the final square brackets need to have a size parameter inside and it needs to be known at compile time.

I've looked around and found this answer which, I believe, has the best solution. The idea is to pass the 2 dimensions separately and the location of the data. So, the function's signature would be like this:

void print(void *arr, size_t rows, size_t cols);

Then, we create a temporary array, which we can use conventionally, like this:

int (*temp)[cols] = (int (*)[cols]) arr;

I wrote some code to test this and it works.

#include <stdio.h>

void fill(void * arr, size_t rows, size_t cols, int val);
void print(void * arr, size_t rows, size_t cols);

int main(void) {
  size_t iArr = 5, jArr = 3;
  int arr[iArr][jArr];
  fill(arr, iArr, jArr, 0x45);
  print(arr, iArr, jArr);
  return 0;
}

void fill(void * arr, size_t rows, size_t cols, int val)
{
  int (*temp)[cols] = (int (*)[cols])arr;
  for(size_t i = 0; i < rows; i++)
  {
    for(size_t j = 0; j < cols; j++)
    {
      temp[i][j] = val;
    }
  }
  arr = temp;
}

void print(void * arr, size_t rows, size_t cols)
{
  int (*temp)[cols] = (int (*)[cols])arr;
  for(size_t i = 0; i < rows; i++)
  {
    for(size_t j = 0; j < cols; j++)
    {
      printf("%3d ", temp[i][j]);
    }
    putchar('\n');
  }
}

This can also be tested here. I'm curious whether this will work always without errors. I tested it with all kinds of sizes and, again, no problem there. Can this be improved further? Could there be any edge cases where this breaks (checks for valid arguments or making sure the size passed doesn't exceed the stack size are omitted here, as that's not the point of this question).

\$\endgroup\$
3
  • \$\begingroup\$ "Could there be any edge cases where this breaks" --> Yes, when VLAs are not allowed (req'd in c99, optional C11, C17/18). Are you looking for a non-VLA solution? \$\endgroup\$ Feb 5, 2020 at 16:59
  • \$\begingroup\$ Tip: to the number of items in an array, use size_t length = sizeof(array) / sizeof(array[0]); \$\endgroup\$ Feb 6, 2020 at 10:32
  • \$\begingroup\$ regarding: void print(void *arr, size_t rows, size_t cols); It is much easier (and less error prone) to use: void print( size_t rows, size_t cols, int arr[ rows ][ cols ] ) Note: the compiler really needs to know the type of each element in the array \$\endgroup\$ Feb 6, 2020 at 16:33

3 Answers 3

7
\$\begingroup\$

Type-wise, it is ok to go from void pointers to array pointers and back. As long as the "effective type" is an int array of the specified size. Void pointers do however have non-existent type safety, so they should be avoided for that reason.

The best way is rather to use an array and let the compiler "adjust" it to an array pointer between the lines:

void fill (size_t rows, size_t cols, int arr[rows][cols], int val);

The VLA syntax requires that rows and cols exist, so arr must be declared on the right side of them in the parameter list. Please note that this is still a pointer and not a whole VLA passed by value - we can't pass arrays by value in C.

Fixed example:

#include <stdio.h>

void fill(size_t rows, size_t cols, int arr[rows][cols], int val);
void print(size_t rows, size_t cols, int arr[rows][cols]);

int main(void) {
  size_t iArr = 5, jArr = 3;
  int arr[iArr][jArr];
  fill(iArr, jArr, arr, 0x45);
  print(iArr, jArr, arr);
  return 0;
}

void fill(size_t rows, size_t cols, int arr[rows][cols], int val)
{
  for(size_t i = 0; i < rows; i++)
  {
    for(size_t j = 0; j < cols; j++)
    {
      arr[i][j] = val;
    }
  }
}

void print(size_t rows, size_t cols, int arr[rows][cols])
{
  for(size_t i = 0; i < rows; i++)
  {
    for(size_t j = 0; j < cols; j++)
    {
      printf("%3d ", arr[i][j]);
    }
    putchar('\n');
  }
}
\$\endgroup\$
5
  • \$\begingroup\$ I thought you weren't allowed to put variables in square brackets of function signatures. This makes it a lot simpler. \$\endgroup\$ Feb 5, 2020 at 15:50
  • 1
    \$\begingroup\$ "The VLA syntax requires that rows and cols exist, so arr must be declared on the right side of them in the parameter list." --> Yes, except that int arr[rows][cols] could be int arr[1][cols] with rows on the right of arr. Yet what you coded is better. \$\endgroup\$ Feb 5, 2020 at 16:56
  • 1
    \$\begingroup\$ If you passa a struct array_wrapper { array_type array[ARRAYLEN]; }, you are effectively passing an array by value ;-) [Ok, is not VLA...] \$\endgroup\$
    – Astrinus
    Feb 6, 2020 at 9:06
  • 1
    \$\begingroup\$ @Astrinus I am aware. But doing so is horrible practice and not something we should teach. \$\endgroup\$
    – Lundin
    Feb 6, 2020 at 9:11
  • \$\begingroup\$ I agree, especially becaue I have seen passing arrays of 50k IEEE754 Binary64 values. By value. Every 5 ms. And the array changed every ten minutes or so. \$\endgroup\$
    – Astrinus
    Feb 7, 2020 at 9:17
5
\$\begingroup\$

One suggestion will be to put your parameters on a dedicated struct

struct my_array {
    void *data;
    size_t rows;
    size_t cols;
}

So your functions will only have one parameter as input.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ This will come with lots of needless de-referencing though, and the type safety problem is still there. \$\endgroup\$
    – Lundin
    Feb 5, 2020 at 15:37
  • 1
    \$\begingroup\$ @Lundin " lots of needless de-referencing " --> doubt that - depends on code and compiler. Definitely agree on type safety issue. \$\endgroup\$ Feb 5, 2020 at 16:53
  • \$\begingroup\$ @chux-ReinstateMonica It's pretty much dead certain, when the struct is passed by pointer to a function. See for yourself: godbolt.org/z/XkR6tt. I tried every compiler & target available on Godbolt and the struct version gave less efficient code in every single case. \$\endgroup\$
    – Lundin
    Feb 6, 2020 at 7:51
  • \$\begingroup\$ @Lundin Sample code demos 1 more de-referencing that passing by value. One does not equate to "lots of needless de-referencing" IMO. Code with lots of uses * struct my_array . data would be expected to de-reference once to form `* struct my_array . data``. Still assert depends on code and compiler. In the end, the use an extra level of direction remains linear performance change. \$\endgroup\$ Feb 6, 2020 at 15:07
4
\$\begingroup\$

variable length arrays are required in C99 and optional in C11, C17/18.

To detect, something like

#if defined(__STDC__) && defined(__STDC_VERSION__) && \
    (__STDC_VERSION__ == 199901 || (__STDC_VERSION__ >= 201112 && __STDC_NO_VLA__ != 1))
    #define VLA_OK 1
#else
    #define VLA_OK 0
#endif

If code does not use variable length arrays, code could take advantage that the address of the first element of the 2D array is equivalent to &arr[0][0] and that 2D arrays are continuous.

void fill(void *arr, size_t rows, size_t cols, int val) {
   int *a = arr;
   for(size_t i = 0; i < rows; i++) {
     for(size_t j = 0; j < cols; j++) {
       *a++ = val;
    }
  }
}

Sample usage

#define IN 5
#define JN 3
int arr[IN][JN];
fill(arr, IN, JN, 0x45);

This does lose type checking.
Code instead could oblige passing the address of the first int

//        v---v---- type checked
void fill(int *arr, size_t rows, size_t cols, int val) {
   for(size_t i = 0; i < rows; i++) {
     for(size_t j = 0; j < cols; j++) {
       *arr++ = val;
    }
  }
}

Sample usage

#define IN 5
#define JN 3
int arr[IN][JN];
fill(&arr[0][0], IN, JN, 0x45);
// or
fill(arr[0], IN, JN, 0x45);
\$\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.