std::array
automates the allocation and deallocation of memory. std::array
is a templatized class that takes two parameters – the type of the elements and the size of the array.
In the following example, we will declare std::array
of int
of size 10
, set the value of any of the elements, and then print that value to make sure it works:
std::array<int, 10> arr; // array of int of size 10
arr[0] = 1; // Sets the first element as 1
std::cout << "First element: " << arr[0] << std::endl;
std::array<int, 4> arr2 = {1, 2, 3, 4};
std::cout << "Elements in second array: ";
for(int i = 0; i < arr.size(); i++)
std::cout << arr2[i] << " ";
This example would produce the following output:
First element: 1
Elements in second array: 1 2 3 4
As we can see, std::array
provides operator[]
, which is same as the C-style array, to avoid the cost of checking whether the index is less than the size of the array. Additionally, it also provides a function called at(index)
, which throws an exception if the argument is not valid. In this way, we can handle the exception in an appropriate manner. So, if we have a piece of code where we will be accessing an element with a bit of uncertainty, such as an array index being dependent on user input, we can always catch the error using exception handling, as demonstrated in the following example.
try
{
std::cout << arr.at(4); // No error
std::cout << arr.at(5); // Throws exception std::out_of_range
}
catch (const std::out_of_range& ex)
{
std::cerr << ex.what();
}
Apart from that, passing std::array
to another function is similar to passing any built-in data type. We can pass it by value or reference, with or without const
. Additionally, the syntax doesn't involve any pointer-related operations or referencing and de-referencing operations. Hence, the readability is much better compared to C-style arrays, even for multidimensional arrays. The following example demonstrates how to pass an array by value:
void print(std::array<int, 5> arr)
{
for(auto ele: arr)
{
std::cout << ele << ", ";
}
}
std::array<int, 5> arr = {1, 2, 3, 4, 5};
print(arr);
This example would produce the following output:
1, 2, 3, 4, 5
We can't pass an array of any other size for this function, because the size of the array is a part of the data type of the function parameter. So, for example, if we pass std::array<int, 10>
, the compiler will return an error saying that it can't match the function parameter, nor can it convert from one to the other. However, if we want to have a generic function that can work with std::array
of any size, we can make the size of the array templatized for that function, and it will generate code for all the required sizes of the array. So, the signature will look like the following:
template <size_t N>
void print(const std::array<int, N>& arr)
Apart from readability, while passing std::array
, it copies all the elements into a new array by default. Hence, an automatic deep copy is performed. If we don't want that feature, we can always use other types, such as reference and const
reference. Thus, it provides greater flexibility for programmers.
In practice, for most operations, std::array
provides similar performance as a C-style array, since it is just a thin wrapper to reduce the effort of programmers and make the code safer. std::array
provides two different functions to access array elements – operator[]
and at()
. operator[]
, is similar to C-style arrays, and doesn't perform any check on the index. However, the at()
function provides a check on the index, and throws an exception if the index is out of range. Due to this, it is a bit slower in practice.
As mentioned earlier, iterating over an array is a very common operation. std::array
provides a really nice interface with the help of a range for loops and iterators. So, the code for printing all the elements in an array looks like this:
std::array<int, 5> arr = {1, 2, 3, 4, 5};
for(auto element: arr)
{
std::cout << element << ' ';
}
This example would show the following output:
1 2 3 4 5
In the preceding example, when we demonstrated printing out all of the elements, we iterated using an index variable, where we had to make sure that it was correctly used according to the size of the array. Hence, it is more prone to human error compared to this example.
The reason we can iterate over std::array
using a range-based loop is due to iterators. std::array
has member functions called begin()
and end()
, returning a way to access the first and last elements. To move from one element to the next element, it also provides arithmetic operators, such as the increment operator (++
) and the addition operator (+
). Hence, a range-based for
loop starts at begin()
and ends at end()
, advancing step by step using the increment operator (++
). The iterators provide a unified interface across all of the dynamically iterable STL containers, such as std::array
, std::vector
, std::map
, std::set
, and std::list
.
Apart from iterating, all the functions for which we need to specify a position inside the container are based on iterators; for example, insertion at a specific position, deletion of elements in a range or at a specific position, and other similar functions. This makes the code more reusable, maintainable, and readable.
Note
For all functions in C++ that specify a range with the help of iterators, the start()
iterator is usually inclusive, and the end()
iterator is usually exclusive, unless specified otherwise.
Hence, the array::begin()
function returns an iterator that points to the first element, but array::end()
returns an iterator just after the last element. So, a range-based loop can be written as follows:
for(auto it = arr.begin(); it != arr.end(); it++)
{
auto element = (*it);
std::cout << element << ' ';
}
There are some other forms of iterators, such as const_iterator
and reverse_iterator
, which are also quite useful. const_iterator
is a const
version of the normal iterator. If the array is declared to be a const
, its functions that are related to iterators, such as begin()
and end()
, return const_iterator
.
reverse_iterator
allows us to traverse the array in the reverse direction. So, its functions, such as the increment operator (++
) and advance
, are inverses of such operations for normal iterators.
Besides the operator[]
and at()
functions, std::array
also provides other accessors, as shown in the following table:
Figure 1.6: Table showing some accessors for std::array
The following snippet demonstrates how these functions are used:
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << arr.front() << std::endl; // Prints 1
std::cout << arr.back() << std::endl; // Prints 5
std::cout << *(arr.data() + 1) << std::endl; // Prints 2
Another useful functionality provided by std::array
is the relational operator for deep comparison and the copy-assignment operator for deep copy. All size operators (<
, >
, <=
, >=
, ==
, !=
) are defined for std::array
to compare two arrays, provided the same operators are also provided for the underlying type of std::array
.
C-style arrays also support all the relational operators, but these operators don't actually compare the elements inside the array; in fact, they just compare the pointers. Therefore, just the address of the elements is compared as integers instead of a deep comparison of the arrays. This is also known as a shallow comparison, and it is not of much practical use. Similarly, assignment also doesn't create a copy of the assigned data. Instead, it just makes a new pointer that points to the same data.
Note
Relational operators work for std::array
of the same size only. This is because the size of the array is a part of the data type itself, and it doesn't allow values of two different data types to be compared.