To answer the question directly: in C and C++ passing static array as an argument needs all dimensions except the outermost. So, if you have N-dimensional array, you need to specify the sizes of N-1 dimensions, except the leftmost one: int array[][4][7][2]...[3]
.
Now, to the gory details.
So, let's say you have int a[3];
. What is the type of a
? It is int [3]
. Now, you want to pass it into function. How do you do this? There are two ways: you can either pass the array by pointer to the first element (works in C and C++), or you can pass the array by reference (references are a C++ thing). Let's consider these examples:
#include <iostream>
void foo(int* array, std::size_t len) { // same as foo(int array[],...)
for (std::size_t i = 0; i < len; i++)
std::cout << array[i] << ' ';
std::cout << '\n';
}
void bar(int (&array)[3]) {
for (int n : array)
std::cout << n << ' ';
std::cout << '\n';
}
template <std::size_t N>
void baz(int (&array)[N]) {
for (int n : array)
std::cout << n << ' ';
std::cout << '\n';
}
int main () {
int a[3] = {1, 2, 3};
int b[4] = {1, 2, 3, 4};
foo(a, 3); // BAD: works, but have to specify size manually
foo(b, 4); // BAD: works, but have to specify size manually
bar(a); // BAD: works, but bar() only accepts arrays of size 3
bar(b); // DOESN'T COMPILE: bar() only accepts arrays of size 3
baz(a); // OK: size is part of type, baz() accepts any size
baz(b); // OK: size is part of type, baz() accepts any size
}
Let's consider foo()
.
foo()
's signature could also be written as void foo(int array[], ...)
. This is purely syntactic sugar and int array[]
means the same as int* array
. Note, however, that this only applies to function signatures, everywhere else these syntaxes are not equivalent.
When you call it as foo(a, 3)
, it's signature is set to accept a pointer to int as first parameter: int* array
. But you know that a
is of type int [3]
, so how does it work? What happens is, the pointer to the first element of the array is passed by value. Which means, it is the same as writing foo(&a[0],...)
. We take the address of the first element, which is then copied into int* array
. As you might notice, having a pointer to the first array doesn't tell us anything about the size of the array, we lost this information during conversion from array type int [3]
to int *
. Which is why we have to supply the second argument that specifies the length of a
. We call this implicit conversion an "array to pointer decay". Decay specifically because we were forced to lose important information -- we had it right there in the type, but now we have to have another argument that describes how many elements the array has. A bit stupid and inconvenient, isn't it?
Now consider bar()
.
In bar()
we pass the array type by reference. This is something that C++ has improved upon C. I will not explain what references are, but in general, you can think of them as something that allows you to get the object the way it is defined, without using any convertions to pointers. In this case, the type of array
remains int [3]
, so we have passed in an array and haven't lost any type information. Great! This means, we can use idiomatic C++ syntax to further improve our code. I have replaced the normal for
loop, as found in foo()
, with a for-each loop, where we just need to supply a variable to store the element (n
) and the array (array
). Note that this is possible only because array
preserves type information! Trying to do this in foo()
would result in a compilation error.
However, there is still a problem with this. bar()
has to have the array size as part of its signature! This means that if a
was any different size, let's say 4 elements, trying to use bar()
would result in a compilation error because int [3]
and int [4]
are incompatible types.
Consider baz()
, which solves the above problem.
Just a little bit of templates will make baz()
usable on arrays of any size, while the usage stays the same as in bar()
.
Now, let's take it to multiple dimensions:
#include <iostream>
void foo2d(int (*array)[3], std::size_t rows, std::size_t cols) {
for (std::size_t i = 0; i < rows; i++)
for (std::size_t j = 0; j < cols; j++)
std::cout << array[i][j] << ' ';
std::cout << '\n';
}
void bar2d(int (&array)[2][3]) {
for (std::size_t i = 0; i < 2; i++)
for (std::size_t j = 0; j < 3; j++)
std::cout << array[i][j] << ' ';
std::cout << '\n';
}
template <std::size_t N, std::size_t M>
void baz2d(int (&array)[N][M]) {
for (std::size_t i = 0; i < N; i++)
for (std::size_t j = 0; j < M; j++)
std::cout << array[i][j] << ' ';
std::cout << '\n';
}
int main () {
int c[2][3] = { {1, 2, 3}, {4, 5, 6} };
foo2d(c, 2, 3);
bar2d(c);
baz2d(c);
}
And again, only baz2d()
doesn't require hardcoded size information.
One more example, of foo3d()
, just to demonstrate what I mean when I say only the outermost dimension doesn't need to be specified:
void foo3d(int (*array)[2][1], std::size_t rows, std::size_t cols, std::size_t K) {
for (std::size_t i = 0; i < rows; i++)
for (std::size_t j = 0; j < cols; j++)
for (std::size_t k = 0; k < K; k++)
std::cout << array[i][j][k] << ' ';
std::cout << '\n';
}
int main () {
int d[3][2][1] = { {{1}, {2}}, {{3}, {4}}, {{5}, {6}} };
foo3d(d, 3, 2, 1);
}
Pay attention to how it's called vs. how it's declared in the signature. So, why do you not need to declare the outermost size? Because only the first pointer decays, due to passing it to the function. d[0][0][0]
stores element of type int
, d[0][0]
stores element of type int [1]
, d[0]
stores element of type int [2][1]
. Which are all themselves are arrays! Well, except d[0][0][0]
, obviously. So, what is the type of the array
in foo3d()
? It's int (*)[2][1]
: pointer to array of size 2, each element of which is an array of size 1.