C-array as function parameter

Hi everyone!
I am learning about the subtleties of C-arrays (yes, I know one should use vectors instead, but one still needs to understand classical arrays when reading other peoples'code).

Here are some examples, my question comes at the end.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// wrong example
void print_arr(int arr[]) { // C-array declaration
// or: void print_arr(int* arr) { 
    int noBytes = sizeof arr; // Wrong! Reason: Same as sizeof(int*)!
    int iter = noBytes/sizeof(int);
    for(int i = 0; i < iter; ++i) {
        cout << arr[i] << ’\n’;
    }
}

int main() {
    int arr[10]; // C-array definition
    // some code
    print_arr(arr); // wrong ...
}


I will come back to this first example at the end.

To make it right, one can pass the number of elements:
1
2
3
4
5
6
// correct example
void print_arr2(int arr[], unsigned int n) {
    for(unsigned int i = 0; i < n; ++i) {
        cout << arr[i] << ’\n’;
    }
}


What to do with 2-dimensional arrays?
The number of elements in the "inner" array, is part of the type of the outer array, so it needs to be spelled out in the function parameters:

1
2
3
4
5
6
7
8
9
10
void print_arr2D(int arr[][3], size_t n)
// or : void print_arr2D(int (*arr)[3], size_t n) {
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < 3; ++j) {    // annoying magic number
            cout << arr[i][j] << ’ ’;
        }
        cout << ’\n’;
    }
    cout << ’\n’;
}


Now, we use a template to make this work for arbitrary columns:
1
2
3
4
5
6
7
8
9
10
11
template<typename arrtype>
void print_arr2DT(arrtype arr, size_t n) {
    const unsigned int cols = sizeof arr[0] /sizeof arr[0][0];
    for(unsigned int i = 0; i < n ; ++i) {
        for(unsigned int j = 0; j < cols; ++j) {
            cout << arr[i][j] << ’ ’;
        }
        cout << ’\n’;
    }
    cout << ’\n’;
}


I understand it until here. But now, the book claims that we can pass the array by reference so that
even the row number n does not need to be passed anymore:
1
2
3
4
5
6
7
8
9
10
11
12
template<typename arrtype>
void print_arr2DT(arrtype &arr) { // pass by reference
    constexpr unsigned int rows = sizeof(arrtype)/ sizeof(arr[0]);    // can use sizeof(arrtype) now
    constexpr unsigned int cols = sizeof arr[0] /sizeof arr[0][0];
    for(unsigned int i = 0; i < rows; ++i) {
        for(unsigned int j = 0; j < cols; ++j) {
            std::cout << arr[i][j] << ’ ’;
        }
        std::cout << ’\n’;
    }
    std::cout << ’\n’;
}


My question: Why could I not have passed by reference in the first example as well? There, the author claimed we can't use sizeof() and need to pass the number of rows manually.
I understood the rasoning that the array name is just a pointer. Passing by reference would still mean that sizeof() would just give the size of the pointer. But why is it different here?




PhysicsIsFun wrote:
Why could I not have passed by reference in the first example as well?

You could. But then you would have to specify the size and the function would only work for arrays of that particular size.

1
2
3
void print_arr(int (&arr)[10]) {
    ...
}


PhysicsIsFun wrote:
I understood the rasoning that the array name is just a pointer. Passing by reference would still mean that sizeof() would just give the size of the pointer.

No, if you pass an array by reference the array type is preserved so the sizeof trick works.

Prefer std::size instead of this sizeof trick. It'll give you a compilation error if you use it on a pointer by mistake instead of silently giving you the wrong result.
Last edited on
Looks like miscommunication.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

void a( int* arr, int n ) {
    std::cout << sizeof( arr ) << '\n';
    for ( int i=0; i<n; ++i ) std::cout << ' ' << arr[i];
    std::cout << "\n\n";
}

template <typename T>
void b( T & arr ) {
    std::cout << sizeof( arr ) << '\n';
    for ( int x : arr ) std::cout << ' ' << x;
    std::cout << "\n\n";
}

int main()
{
  int data[] { 7, 42, 3 };
  std::cout << sizeof( data ) << '\n';
  a( data, sizeof(data) / sizeof(data[0]) );
  b( data );
}

12
4
 7 42 3

12
 7 42 3

sorry, where are you asking to pass by reference? in the first TEMPLATE example or the first example with raw array & pointers?

I think what you are asking is that the template is making a type, and its no longer auto collapsing the new type to a pointer.

I'm personally not much of a fan of multidimensional arrays, at least not when the size can vary.

Sometimes people use an array of pointers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;

void print_arr2D(int** arr, unsigned int cols, unsigned int rows) {
	for (unsigned int i = 0; i < rows; ++i) {
		for (unsigned int j = 0; j < cols; ++j) {
			std::cout << arr[i][j] << ' ';
		}
		std::cout << '\n';
	}
}

int main() {
	unsigned int cols = 20;
	unsigned int rows = 50;
	int** arr = new int*[rows];
	for (unsigned int i = 0; i < rows; ++i) {
		arr[i] = new int[cols];
	}
	// some code
	print_arr2D(arr, cols, rows);
}

But I prefer to use a single array which is more memory compact and efficient (and easier not having to deal with all those memory allocations).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

void print_arr2D(int* arr, unsigned int cols, unsigned int rows) {
	for (unsigned int i = 0; i < rows; ++i) {
		for (unsigned int j = 0; j < cols; ++j) {
			std::cout << arr[i * cols + j] << ' ';
		}
		std::cout << '\n';
	}
}

int main() {
	unsigned int cols = 20;
	unsigned int rows = 50;
	int* arr = new int[cols * rows];
	// some code
	print_arr2D(arr, cols, rows);
}
Last edited on
Note that using a function template is only an option if the caller knows the size at compile time. If you for example did something like this to handle 2D maps in a game you wouldn't be able to use a template function unless all maps had the same size. Instead I would recommend you do it the way I showed above.
Last edited on
yes, I know one should use vectors instead, but one still needs to understand classical arrays when reading other peoples'code


On this note,
you would HOPE that even if reading C code, the author of anything substantial would have crafted a struct with the data and sizes together, as a crude vector-like approach. At this point, important code that did it without at least a simple object would be stuff dating back 25+ years.

The problems you are seeing here is exactly why vectors are so useful. Before they existed, people had to keep reinvented the wheel to work around it by rolling out their own structs and work arounds.

its still good to know this limitation of multidimensional arrays. No matter what you do, you are glued to compile time sizes, really. You can reshape a block of memory (eg 1d to 2d or 3d) with some hand waving, but even that requires compile time dimensions on the 2d or 3d. You can go from multidimensional to 1d all day long without the dimensions, though. And many, many 'tricks' stop working if you use dynamic multidimensional arrays, like int*** type constructs using new, because the memory isn't ensured to be one big block.
With C++20 you'd pass a 1-d c-style array as a std::span type (if you can't use std::vector):

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <span>

void print_arr(std::span<const int> arr) {
	for (const auto& i : arr)
		std::cout << i << '\n';
}

int main() {
	int arr[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // C-array definition

	print_arr(arr);
}


See https://en.cppreference.com/w/cpp/container/span

NB.
span<int> changed to span<const int>
See JLBorges comment below.

Last edited on
seeplus wrote:
With C++20 you'd pass a 1-d c-style array as a std::span type (if you can't use std::vector)

std::span works fine with std::vector too.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
#include <span>

void print_arr(std::span<int> arr) {
	for (const auto& i : arr)
		std::cout << i << '\n';
}

int main() {
	std::vector<int> vec {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	print_arr(vec);
}
Last edited on
> With C++20 you'd pass a 1-d c-style array as a std::span type

Ideally, in a const-correct manner:
void print_arr( std::span< const int > arr ) ;
Good point. I just copy pasted the function from seeplus.
Last edited on
Yeah - my bad. std::span always works with std::array
Last edited on
Topic archived. No new replies allowed.