Reference to Array
C-style Array Variables, Parameters and References
PREREQUISITES — You should already…
- have been writing and compiling C++ programs for a while.
Introduction
For many C and C++ programmers, the mention of simple pointers is often enough to cause unease. This is even worse when the subject of pointer-to-array types comes up. Fortunately, pointer-to-array types are not common, nor very useful. In C++, reference types were added, leading to the corresponding: reference-to-array types.
Reference Fundamentals
A reference type is not a type in any traditional sense; it is basically the name for a syntax that tells the compiler to implicitly:
- obtain the address of the initialising expression, and
- apply the indirection operator when using the reference variable.
A reference type, say T&
, has some relationship with a corresponding pointer type: T*
. Consider the following code extract, using an int
pointer (int* P;
) and an int
reference (int& R;
) to refer to an int
variable (int I;
):
int I = 123; // assume `I` has address `300`.
int* P = &I; // `P` contains the value `300` (explicit address-of).
int& R = I; // `R` contains the value `300` (implicit address-of).
int J = *P; // explicit indirection (`J` now contains `123`).
int K = R; // implicit indirection (`K` now contains `123`).
*P = 111; // explicit indirection (`I` now contains `111`).
R = 222; // implicit indirection (`I` now contains `222`).
cout << &*P; // will output address of `I` (explicit indirect).
cout << & R; // will output address of `I` (implicit indirect).
The memory layout involving P
, and all statements involving P
, will be exactly the same for R
. The only difference between them, is that the compiler automatically initialises R
with an address, and automatically dereferences R
when it is used in an expression.
Reference Parameters
Parameters are simply variables of the functions called, which are allowed to be initialised by callers. They have the same lifetime, namespace and scope as other local variables. If a variable can be of type int&
, so can a parameter.
void f1 (int* p) { *p *= 2; }
void f2 (int& r) { r *= 2; }
int i = 2;
f1(&i); cout << i << endl; // output: 4
f2( i); cout << i << endl; // output: 8
The call to f2()
implicitly initialises the parameter with the address of i
, while we must do it explicitly when calling f1()
. The reference to r
inside f2()
implicitly applies the indirection operator, whereas we have to do it explicitly with p
in f1()
.
Arrays As Parameters
In C++, it is possible to define functions that take parameters of type “reference to array”. This does not refer to the dubious syntax where a pointer parameter may be written as if it is an array:
The above function header is 100% equivalent to:
And so is the following function header, where the size X
is given for the “array”:
The X
can be any constant integer type value, and arr
will still be treated as type int*
. In other words, you can pass it an array of any size. This is a syntactic hangover from C, inherited by C++. It is one more reason not to use C-style arrays in C++; you should rather use std::array
.
A pointer-to-array type looks like this: T(*)[N]
, so a reference-to-array type has a similar syntax: T(&)[N]
. Here they are both in use:
int arr[4]{ 1, 2, 3, 4 };
int (*par)[4] = &arr; // pointer-to-array (explicit address-of).
int (&rar)[4] = arr; // reference-to-array (implicit address-of).
rar [1] = 222; // implicit indirection.
(*par)[2] = 333; // explicit indirection.
// `arr` ⇒ { 1, 222, 333, 4 }
Reference and pointer types are most often used as parameters. Instead of local variables like rar
and par
above, their types could be applied to parameters, as in the following code extract (referring to the same arr
above):
void func1(int (*par)[4])…
void func2(int (&rar)[4])…
func1(&arr); // explicitly pass address
func2(arr); // implicitly pass address (reference)
If the size of arr
should change in its definition, the above function calls will not be valid anymore. Here is a complete example:
arrayparams.cpp
— Array & Pointer Reference
/*!@file arrayparams.cpp
* @brief [c++11] Reference-to-Array & Pointer-to-Array Examples
*/
#include <iostream>
namespace { using std::cout; using std::endl; }
void f0 (int* arr) {
cout << "arr = " << arr << endl; arr[0] = 111;
}
void f1 (int arr[]) {
cout << "arr = " << arr << endl; arr[1] = 222;
}
void f2 (int arr[10]) {
cout << "arr = " << arr << endl; arr[1] = 333;
}
void f3 (int (*par)[5]) {
cout << "arr = " << par << endl; (*par)[2] = 444;
cout << "arr = " << par << endl; par[0][2] = 444; // same thing.
}
void f4 (int (&rar)[5]) {
cout << "arr = " << rar << endl; rar[3] = 555;
}
int main () {
int arr[5]{ 11, 22, 33, 44, 55 };
f0(arr); f1(arr); f2(arr); f3(&arr); f4(arr);
for (auto i : arr) cout << i << ' ';
cout << endl;
int brr[3]{ 1, 2, 3 };
f0(brr); f1(brr); f2(brr);
// f3(&brr); f4(brr); ← won't compile
for (auto i : brr) cout << i << ' ';
cout << endl;
return EXIT_SUCCESS;
}
The output of all the functions will be exactly the same, and each will change another element of the original array (arr
in main
). This can be proved by examining the output of the first for()
loop, which will be: 111 222 333 444 555
.
While you can pass any int
array to f0()
, f1()
and f2()
, this is not true for f3()
and f4()
. The latter two can only take parameters obtained from expressions referring to arrays of 5
int
s exactly (int[5]
).
NOTE — Overloading
As a consequence of the above, you cannot overload f0()
, f1()
, f2()
and f4()
, because the compiler would not be able to determine the correct type from the argument expressions at the point of call.
C-style Array Size Template Function
Although reference-to-arrays are not very common because of the fixed array size limitation, we can use them with C++11 templates to create a constant expression template function that will return the size of any array:
template <typename T, size_t Count>
constexpr size_t ArrSize(T (&)[Count]) noexcept { return Count; }
Because of the constexpr
keyword, the function is evaluated at compile time. It can thus be used anywhere a constant expression is required, even if it may look unusual:
The ArrSize
template function supersedes this common preprocessor macro:
This is another illustration of the power of C++ templates. Note that the above example depends on C++11 upwards (particularly: constexpr
and noexcept
, which are new keywords).
Notes
From C++17 on, you can use the std::size()
function from the <iterator>
header instead of the ArrSize()
above. It works with any container having a size()
member function, and with C-style arrays. Prior to C++17, you could have used the std::extent()
function from the <type_traits>
header (it works with a type though):
using std::cout; using std::extent;
cout << extent<int[2]>::value << '\n'; //⇒ 2
cout << extent<double[3]>::value << '\n'; //⇒ 3
cout << extent<double[4][5]>::value << '\n'; //⇒ 4
Another alternative, would be the std::distance()
function. It requires iterators, but can be used with C-style arrays. It can be generalised more with the std::begin()
function (or cbegin()
), and the matching std::end()
function (or cend()
). These are all from the <iterator>
header.
using std::cout; using std::distance;
using std::begin; using std::end;
int ia[2]; double da[3];
cout << distance(begin(ia), end(ia)) << '\n'; //⇒ 2
cout << distance(begin(da), end(da)) << '\n'; //⇒ 3
2017-11-18: Update to new admonitions. [brx]
2017-10-07: Added C++17
std::size()
, and additional notes. Corrected a comment. [brx]2016-11-19: Created. [brx]