0

I have written a small program that passes a 2D array to 2 separate functions that store and display the pattern of a chessboard. While the program works perfectly fine, I would like to ask a more technical question that I have not been able to answer myself through my searches.

I am wondering how it is possible that my program compiles and runs when passing the 2D array with ONLY the columns variable specified, but not the rows. For instance void setBoard(char chessboard[][cols]);

Here is the link to the program: https://codecatch.net/post/54969994-76d7-414b-aab6-997d3fef895c

Here is the same code for those of you that don't want to click the link:

#include<iostream>
using namespace std;

const int rows = 8;
const int cols = 8;
char chessboard[rows][cols];

void setBoard(char chessboard[][cols]);
void printBoard(char chessboard[][cols]);

void setBoard(char chessboard[][cols]) {
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(i % 2 == 0 && j % 2 == 0) {
                chessboard[i][j] = 'x';
            } else {
                if(i % 2 != 0 && j % 2 == 1) {
                    chessboard[i][j] = 'x';
                } else {
                    chessboard[i][j] = '-';
                }
            }
        }
    }
    return;
}

void printBoard(char chessboard[][cols]) {
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            cout << chessboard[i][j] << " ";
        }
        cout << endl;
    }
    return;
}

int main(int argc, char const *argv[])
{
    setBoard(chessboard);
    printBoard(chessboard);
    return 0;
}
Austin Leath
  • 85
  • 1
  • 1
  • 9
  • 3
    Does this answer your question? [How do I define variable of type int\[\]\[26\]?](https://stackoverflow.com/questions/58771191/how-do-i-define-variable-of-type-int26) I'll also throw in a reference for [arrays of unknown bound](https://en.cppreference.com/w/cpp/language/array#Arrays_of_unknown_bound). (If that's not enough, I could add an answer later.) – JaMiT Feb 21 '21 at 00:28

2 Answers2

3

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.

Ave Milia
  • 599
  • 4
  • 12
2

Parameter of a function is never an array type in C++ (nor in C).

You can declare a function that has an array parameter (as is done in the example program), but that declaration is adjusted so that the parameter is not an array as was written, but it is instead a pointer to element of such array. Example:

void f(int[]);   // will be adjusted
void f(int[42]); // will be adjusted
void f(int*);    // declarations above are adjusted to this

All of the above three declarations declare the same function. If the type of parameter is "array of int", then the type of the element of such array is "int" and pointer to such element is "pointer to int". Notice that the size of the array has no effect on the type of the element. As such, the size has no effect on the declaration in any way, and indeed arrays of unknown bound are allowed in parameter declaration.

Note that this adjustment occurs only in function parameters, and nowhere else. An array is not a pointer.

So, when you declare a function void setBoard(char chessboard[][cols], the parameter chessboard is not an array, because a parameter is never an array. It has been adjusted to be a pointer to element of char [][cols]. Element of such array is char[cols] i.e. array of cols number of char, therefore the adjustead parameter type is pointer to array of cols number of char i.e. char(*)[cols].

You cannot have pointer type to an array of unknown bound, so you cannot leave out cols. But you can leave out rows because as noted above, the size of the declared array parameter is ignored when the type is adjusted to be a pointer to the element of that array.


You may be wondering "if the parameter is actually not an array, then why can an array be passed as argument?". The answer is that there is another rule complementing the parameter adjustment (in simple words): Arrays implicitly convert to the pointer to element type. The result of the conversion is pointer to the first element of that array. Such conversion is called "decaying". This conversion happens automatically whenever the value of an array is used. Example:

printBoard(&chessboard[0]);
printBoard(chessboard);

The above function calls do exactly the same thing. The former explicitly passes pointer to first element, while the latter does the same thing by implicit "decay".

eerorika
  • 232,697
  • 12
  • 197
  • 326