Passing 2D table as function parameter

Hi guys,
I'm trying to implement a function to do some 2D / bilinear interpolation for me. It will be a function that accepts a 2D table as a parameter to lookup from. I've got all the interpolation maths under control. But I'm struggling with passing the table as a parameter to the function. The function needs to accept all different sizes of tables.
This is what I've boiled it down to after chasing the problem:

uint16_t some2Dtable[4][4] = {
  {2, 4, 6, 8},
  {3, 5, 7, 9},
  {4, 6, 8, 10},
  {5, 7, 9, 11}
};

uint16_t useThatTableForSomething(uint16_t **table){
    return table[1][1];
}

void loop() {
  uint16_t value = useThatTableForSomething((uint16_t**)some2Dtable);
  Serial.println(value);
  delay(5000);
}

In this example I'm expecting to get "5" sent back on the serial monitor, but I get "0".

Originally I tried using the function in loop by simply:

uint16_t value = useThatTableForSomething(some2Dtable);

But received a complier error:

error: cannot convert 'uint16_t (*)[4] {aka unsigned int (*)[4]}' to 'uint16_t** {aka unsigned int**}' for argument '1' to 'uint16_t useThatTableForSomething(uint16_t**)

What am I doing wrong? I'm fairly sure its something wrong with the way I'm passing the table to the function, but I don't understand why.

Thanks in advance

The easiest way is to pass a pointer to the table and the 2 sizes as parameters.

To access element at position matrix[i][j] you do *(matrix + j + (width * i))

template <size_t Rows, size_t Cols>
uint16_t useThatTableForSomething(const uint16_t (&table)[Rows][Cols]) {
    return table[1][1];
}

If you call the function with many different sizes, this might generate a lot of code though, in which case you could consider passing the tables as a struct, for example:

struct Table {
  const uint16_t *data;
  size_t rows;
  size_t cols;
};

the following compiles

uint16_t some2Dtable[4][4] = {
    {2, 4, 6, 8},
    {3, 5, 7, 9},
    {4, 6, 8, 10},
    {5, 7, 9, 11}
};

uint16_t useThatTableForSomething (
    uint16_t table [][4])
{
    return table[1][1];
}

void loop() {
    uint16_t value = useThatTableForSomething (some2Dtable);
    Serial.println(value);
    delay(5000);
}

void setup() {
    Serial.begin (9600);
}

But it doesn't address this:


A 2D array and an array of pointers are two entirely different data structures, you cannot implicitly convert one to the other.

i understand why it's been pointed out
... but that wasn't his question

OP said

Thanks guys! :slight_smile:

Here is one way to work with an arbitrary-sized 2D array of a known type:

uint16_t some2Dtable[4][4] =
{
  {2, 4, 6, 8},
  {3, 5, 7, 9},
  {4, 6, 8, 10},
  {5, 7, 9, 11}
};
const size_t some2Dtable_width = sizeof some2Dtable[0] / sizeof some2Dtable[0][0];
const size_t some2Dtable_height = sizeof some2Dtable   / sizeof some2Dtable[0];

void display2Dtable(uint16_t *table, size_t width, size_t height)
{
  for (size_t i = 0; i < height; i++)
  {
    for (size_t j = 0; j < width; j++)
    {
      Serial.print(table[width * i + j]);
      if (j < width - 1)
        Serial.print(F(", "));
    }
    Serial.println();
  }
}

void setup()
{
  Serial.begin (115200);
  delay(200);

  display2Dtable(&some2Dtable[0][0], some2Dtable_width, some2Dtable_height);
}

void loop() {}

Or you could probably just do

display2Dtable(some2Dtable, some2Dtable_width, some2Dtable_height);

I tried that and got an error. Something about not being able to convert "uint16_t [4][4]" to "uint16_t *". Feel free to try it for yourself.

https://en.cppreference.com/w/cpp/language/array

Note that when array-to-pointer decay is applied, a multidimensional array is converted to a pointer to its first element (e.g., a pointer to its first row or to its first plane): array-to-pointer decay is applied only once.

I’m just on my phone so could not try, but true indeed it’s a 2D array

Unfortunately, this invokes undefined behavior: you're only allowed to index from table[0] through table[width] and dereference from 0 to width-1.
This is because some2Dtable[0] is an array of length width, so anything beyond that would be out of bounds. Or put another way, table[n] is equivalent to some2Dtable[0][n] which is only allowed for 0 <= n <= width (or 0 <= n < width if you use the result).
AFAIK, it doesn't matter that the array is part of a multidimensional array.

I don't know of an alternative, though, except for: don't use multidimensional arrays if you need to do things like this.
Instead, allocate a one-dimensional array and index it yourself, preferably through a class that supports the matrix[row][col] or matrix(row, col) syntax, using a tested matrix or tensor library.

Can you point to the part of the C++ standard where a pointer to a [4][4] array is not equivalent to a [16] array?

It just doesn't say that that's the case, so you cannot assume it is.

About multidimensional arrays, it just says [dcl.array]:

[Note 3 : When several “array of” specifications are adjacent, a multidimensional array type is created; only the first of the constant expressions that specify the bounds of the arrays can be omitted.

[Example 4 :

int x3d[3][5][7];

declares an array of three elements, each of which is an array of five elements, each of which is an array of seven integers. The overall array can be viewed as a three-dimensional array of integers, with rank 3 × 5 × 7. Any of the expressions x3d, x3d[i], x3d[i][j], x3d[i][j][k] can reasonably appear in an expression. The expression x3d[i] is equivalent to *(x3d + i); in that expression, x3d is subject to the array-to-pointer conversion (7.3.3) and is first converted to a pointer to a 2-dimensional array with rank 5 × 7 that points to the first element of x3d. Then i is added, which on typical implementations involves multiplying i by the length of the object to which the pointer points, which is sizeof(int)×5 × 7. The result of the addition and indirection is an lvalue denoting the i th array element of x3d (an array of five arrays of seven integers). If there is another subscript, the same argument applies again, so x3d[i][j] is an lvalue denoting the j th array element of the i th array element of x3d (an array of seven integers), and x3d[i][j][k] is an lvalue denoting the k th array element of the j th array element of the i th array element of x3d (an integer). — end example]

The first subscript in the declaration helps determine the amount of storage consumed by an array but plays no other part in subscript calculations. — end note]

Recall that the built-in subscript operator is just syntactic sugar for pointer arithmetic, i.e. a[b] == *(a + b), about which the standard says:

When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.
(4.1) — If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
(4.2) — Otherwise, if P points to an array element i of an array object x with n elements (9.3.4.5), the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) array element i + j of x if 0 ≤ i + j ≤ n and the expression P - J points to the (possibly-hypothetical) array element i − j of x if 0 ≤ i − j ≤ n.
(4.3) — Otherwise, the behavior is undefined.

So it's OK to go off the end of an array by one element but not two?!?

Only to compute the pointer. You cannot dereference it.

int array[] { 1, 2, 3 };
int *p = array + 3; // ok
*p = 42;            // bad
int *q = array + 4; // bad

This is useful for defining a range, for example, the (noninclusive) end of the range could be a pointer to one past the array.

To go back to the original question consider this example:

struct S {
  int a, b;
};

void some_func(S *);

int main() {
  S array[2] {
    {11, 12},
    {21, 22},
  };
  some_func(&array[0]);
  return array[1].a;
}

Could the optimizer replace the final statement by return 21?

In this case, the answer is no, because the pointer &array[0] can be used to reach array[1], and some_func might have changed it.

If, on the other hand, you would have:

void some_func(int *);

int main() {
  S array[2] {
    {11, 12},
    {21, 22},
  };
  some_func(&(array[0].a));
  return array[1].a;
}

Now the answer is yes, because the pointer &(array[0].a) cannot be used to reach array[1].a.

The same logic can be applied when you use an array instead of a struct:

using S = int[2];

void some_func(int *);

int main() {
  S array[2] {
    {11, 12},
    {21, 22},
  };
  some_func(&(array[0][0]));
  return array[1][0];
}

Seems like a type of

using TwoDimArray = std::array<std::array<uint16_t, NCOLS>, NROW>;

would be a place to start for this. Unfortunately, the size of a std:array is part of its type. So, you can’t call a common function with different array sizes without using templates (in which case it’s not really a common function).

Ideally, you could wrap the 2-Dim std:array composite in a common container class that also keeps track of the dimensions. Then a 2D array of any size could be passed to a common function. Unfortunately, my C++ skills at creating STL-like containers are non-existent.

EDIT:
Ooops ... never mind ... won't work. std::array doesn't use dynamic storage. So, can't make common class as different array sizes would mean different object sizes. std::vector maybe.