In C++, a talk of passing arrays to functions by reference is estranged by the generally held perception that arrays in C++ are always passed by reference. What makes this subject so interesting is to do with the way C++ compiles the array function parameters. By array, we mean the C-style or regular array here, not the C++11 std::array<T, size_t>. The difference is important and would be apparent later in the article.
These are the standard ways to pass regular C-style arrays to functions:
void Foo(char *array, size_t len);
void Foo(char array[], size_t len);
//void Foo(char array[5], size_t len); //Dimension size is ignored
In all the above three cases, the array parameter does not have the size and dimension information of the passed argument. You cannot get the size of the array within Foo by merely calling sizeof(array)
, because a passed array argument to Foo is decayed to a pointer. Therefore, sizeof(array)
would return the size of a char
pointer. Accordingly, the functions that take array parameters, ordinarily, also have an explicit length parameter because array parameters do not have the implicit size information. Here is one implementation of Foo that we would use for comparison later:
void Foo(char array[], size_t len) {
for(size_t i=0; i < len; i++) {
std::cout << array[i] << "\n";
}
}
int main() {
char arr[5] = {'A','B','C','D','E'};
//Call Foo
Foo(arr, 5); //Explicit length passed
return 0;
}
There are numerous disadvantages in treating arrays as pointers. For one, it is often necessary to null-check pointers for safety protocols. Also, as pointers do not have array dimension information, they are not compatible with the modern C++ features like the range-based-for loop.
Nevertheless, in C++, there is a lesser-known syntax to declare a reference to an array:
//'array' is a reference to char [5]
char (&array) [5];
And a function can be declared to take a reference to an array parameter, as follows:
void Foo(char (&array)[5]);
Passing an array to a function that takes an array by reference does not decay the array to a pointer and preserves the array size information. Moreover, we can create aliases to arrays to make their references more palatable. Here is a function that takes a 5-char array by reference with a dandy range-based-for loop:
//Alias of a char[5]
using FiveCharCode = char[5];
//'code' is a char(&)[5]
void Bar(const FiveCharCode& code) {
for(char c : code) { //range-based-for loop works
std::cout << c << "\n";
}
}
int main() {
char code[5] = {'A','B','C','D','E'};
//Call Bar
Bar(code); //No explicit length passed
return 0;
}
Array references open up a few other exciting possibilities. For instance, the function template below can also be called on a standard array besides an STL container. Additionally, the use of templates can even avoid the unsightly array reference syntax and make the code work for any array size:
template<typename _Ret, //Return type
typename _Coll> //Collection type
_Ret Sum(const _Coll& c) {
_Ret sum = 0;
for(auto& v : c)
sum += v;
return sum;
}
int main() {
//With regular array. Passed as reference
int arr[] = {1,2,3,4,5};
std::cout << Sum<int64_t>(arr) << "\n"; //15
//With vector
std::vector<int> vec = {1,2,3,4,5};
std::cout << Sum<int64_t>(vec) << "\n"; //15
return 0;
}
Let's take another example of passing an array by reference. A function template, Increment, takes an array of bytes (unsigned char
) by reference and increments all the bytes in it. Increment works with a byte array of any length:
template<typename ByteArray>
void Increment(ByteArray& a) {
//increment all bytes in array 'a'
for(auto& b : a)
b++;
}
We call Increment on a local int
variable by casting it to a 4-byte array reference and then print the variable, as follows:
int main() {
unsigned int a = 0x01010101;
//Casting int to 'unsigned char' array reference.
Increment( (unsigned char(&)[4])a );
//Print int in hex format.
//Specify width-8 padded with 0 for leading 0.
std::printf("0x%08x\n", a);
return 0;
}
What do you think the output/outcome of the above? Select the correct answer below (check Explanations
for details on the answer):
In this article, we described the references to C-style arrays and why they might be better than pointers in some situations. For the same reasons as described above, the C++11 introduced an array wrapper type std::array<T, size_t>. An std::array instance encloses a C-style array and provides an interface to query the size, along with some other standard container interfaces. std::array can be passed by reference, and because it is a struct, it is possible even to pass it by value too:
//#include <array>
//Get std::array by ref
void
ProcessRef(const std::array<int, 5>& arr) {
for(auto v : arr)
std::cout << v << "\n";
//OR
for(size_t i = 0; i < arr.size(); i++)
std::cout << arr[i] << "\n";
}
//Get std::array by value
void
ProcessValue(std::array<int, 5> arr) {
//modify the elements of local arr
for(auto& v : arr)
v++;
for(size_t i = 0; i < arr.size(); i++)
std::cout << arr[i] << "\n";
}
int main() {
std::array<int, 5> arr = {1,2,3,4,5};
ProcessRef(arr); //Passes ref
ProcessValue(arr); //Passes a copy of arr
return 0;
}
We should prefer std::array over regular C-style arrays wherever possible. However, given the massive existing C++ codebases and the established proclivities in the subconscious of developers, it would be naive to assume that the use of C-style arrays is going to disappear anytime soon. Nonetheless, for whatever reasons, intellectual curiosity or practical, understanding of array references is essential for C++ programmers.