std::array
std::array
- an object that can store a collection of other objectsInevitably, we will want to store a group of data that has the same type. For example, let's imagine we have 5Â characters.
class Character {};
Character Frodo;
Character Gandalf;
Character Gimli;
Character Legolas;
Character Aragorn;
We may want to group these characters together, to form a party. This is where arrays come in
Under the hood, arrays are a contiguous block of memory, big enough to store all our objects. Therefore, to create an array, we need to know the type of objects it will contain, and how many of them we need to store.
std::array
The standard library has an implementation of arrays that we can use, to familiarise ourselves with the concept. Remember, it is the concept that is important - not the particular implementation within the standard library.
There are hundreds of implementations of arrays in C++ that we can use, or we can even create our own once we learn more advanced topics. We're using the version in the standard library here just because it is easy to include in our project, and is a good introduction to the key concepts.
To use std::array
, we need to #include <array>
. We can then declare an array by giving it a name and specifying the type and quantity of things it will contain. The following example shows how we can declare an array that stores 5Â integers:
#include <array>
std::array<int, 5> MyArray;
We can initialize the values at the same time:
#include <array>
std::array<int, 5> MyArray { 1, 2, 3, 4, 5 };
When we initialize the values at the same time we declare the array, we can optionally remove the type and size. This lets the compiler infer it.
To do this, the compiler is using Class Template Argument Deduction (CTAD), which we covered in our earlier lessons on templates:
#include <array>
std::array MyArray { 1, 2, 3, 4, 5 };
std::array
ObjectsA key thing to note about std::array
is that the array has a static size. This means the size of the array must be known at compile time, and the size can not change through the lifecycle of our program.
We can use an expression to set the size of our array, but the result of that expression must be known at compile time. Something like this would work, because the compiler can figure out what integer is returned by the expression:
#include <array>
constexpr int GetSize() {
return 2 + 3;
}
std::array<int, GetSize()> MyArray;
However, if the compiler cannot determine what the result of the expression is, it will throw an error. This means we cannot set or change the size of a std::array
at run time:
std::array<int, GetUserInput()> MyArray;
In the beginner course, we introduced std::vector
, which is an array that can dynamically resize itself at runtime
std::array
ContainersWe can access the members of our array using the MyArray[x]
notation, where x
is the index of the element we want to access. The index of an element within an array is just its position.
However, we start counting from 0. This means the first element of the array is at index 0
, the second element is at index 1
, and so on.
For example, to access the elements of our array, we would do this:
#include <array>
std::array MyArray{1, 2, 3, 4, 5};
int FirstElement{MyArray[0]};
int SecondElement{MyArray[1]};
int LastElement{MyArray[4]};
Note that because we start counting from 0, this also means the last element of an array is at an index of 1 less than its size. For an array of size 5, the last element is at index 4.
As with all values, the index can be derived from any expression that results in an integer:
#include <array>
#include <iostream>
int CalculateIndex(){ return 1 + 1; }
int main(){
using std::cout, std::array;
array MyArray{"First", "Second", "Third"};
// Log out the element at index 0
cout << MyArray[3 - 3] << "\n";
// Log out the element at index 1
int Index{1};
cout << MyArray[Index] << "\n";
// Log out the element at index 2
cout << MyArray[CalculateIndex()] << "\n";
}
This code logs out elements at indices 1, 2, and 3 in order:
First
Second
Third
at()
methodThere is an alternative to the []
operator - elements can be accessed by passing their index to the at()
 method:
#include <array>
#include <iostream>
int main(){
using std::cout, std::array;
array MyArray{"First", "Second", "Third"};
// Log out the element at index 0
cout << MyArray.at(0);
}
First
The main difference between []
and at()
is that the []
method does not perform bounds-checking on the index we pass.
For example, if our MyArray
only has a size of 5
, and we try to access MyArray[10]
, our program’s behavior will become unpredictable, often resulting in a crash.
The at()
method checks if the index we provide as an argument is appropriate for the size of the array. If the argument passed to at()
is out of range, an exception will be thrown. We cover exceptions in detail later in this course.
However, in most cases, we should not use at()
. This is because the additional check performed by at()
has a performance cost, and it really shouldn't be necessary.
This is because, when we build our program in “debug” mode, most compilers will perform bounds checking on the []
operator anyway. We will get an alert if our index is out of range.
This means we’ll be able to see any out-of-range issues during the development of our program and then, when we build in release mode, those checks are removed. As such, using []
typically gets almost all of the benefits of at()
, with none of the performance overhead once we release our software.
std::size_t
typeThere is an issue with using int
values as the index of arrays: the size of arrays can be larger than the maximum value storable in an int
.
To deal with this problem, we have the std::size_t
data type. size_t
is guaranteed to be large enough to match the largest possible size of the array and other objects.
Because of this, it is the recommended way of storing array indices:
std::size_t SomeIndex;
std::size_t CalculateIndex(){
// ...
}
std::array
ContainersOur above example uses arrays with simple integers, but we can store any type in our array.
#include <utility>
#include <array>
class Character {};
int main(){
// A party of 5 characters
std::array<Character, 5> A;
// A party of 5 character pointers
std::array<Character*, 5> B;
// A party of 5 const character pointers
std::array<const Character*, 5> C;
// A collection of 5 pairs
std::array<std::pair<int, bool>, 5> D;
}
Arrays can also store other arrays. This creates "multidimensional arrays". For example, a 3x3 grid could be represented as an array of 3 rows, each row being an array of 3Â items
#include <array>
std::array<std::array<int, 3>, 3> MyGrid{
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
}};
int TopLeft{MyGrid[0][0]};
int BottomRight{MyGrid[2][2]};
Often, C++ types can become very complex. This can make our code hard to read, or just frustrating to use if we are constantly repeating a huge type name in our code.
We can use a using
statement to create a simpler alias for our types. Here, we alias our complex array type to the much simpler name of Grid
:
#include <array>
using Grid = std::array<std::array<int, 3>, 3>;
Grid MyGrid{
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
}};
We covered type aliases in more detail in our lessons on Types and Templates, earlier in the course.
We can also have pointers to arrays:
#include <array>
// Pointer to an array
std::array<std::array<int, 3>, 3>* MyGridPtr;
// Pointer to an array with a using statement
using Grid = std::array<std::array<int, 3>, 3>;
Grid* AliasedPointer;
We can also have references to arrays, with or without const
:
#include <iostream>
#include <array>
using Grid = std::array<std::array<int, 3>, 3>;
void SetTopLeft(Grid& GridToChange, int Value){
GridToChange[0][0] = Value;
}
void LogTopLeft(const Grid& GridToLog){
std::cout << GridToLog[0][0];
}
std::array
with a for
LoopA common task we have when working with arrays is to loop over every element in them, in order to do something to each object.
We could do this with a for
 loop:
#include <iostream>
#include <array>
int main(){
std::array MyArray{1, 2, 3};
for (std::size_t i{0}; i < 3; i++) {
std::cout << MyArray[i];
}
}
123
Line 3, highlighted above, uses the i < 3
conditional, as we know our array has a size of 3
. However, if we add or remove an integer from our array on line 1, we would need to remember to also change line 3, as the size will no longer be 3
.
And we don’t always know the array size anyway - for example, we might be writing a function that receives an array of potentially any size.
We can make our code a little smarter - std::array
has a size()
method that, predictably, returns the size of the array. The following code will log out 3
:
#include <iostream>
#include <array>
int main(){
std::array MyArray{1, 2, 3};
std::cout << MyArray.size();
}
3
We can update our loop to use this method:
#include <iostream>
#include <array>
int main(){
using std::size_t;
std::array MyArray{1, 2, 3};
for (size_t i{0}; i < MyArray.size(); i++) {
std::cout << MyArray[i];
}
}
123
Often, we usually don’t need to work with indices at all - we just want to iterate over everything in the array.
We can do that using a range-based for loop, which looks like this:
#include <iostream>
#include <array>
int main(){
std::array MyArray{1, 2, 3};
for (int& Number : MyArray) {
std::cout << Number;
}
}
123
We cover range-based for loops in more detail later in the course.
— Added a section covering the at()
method.
— Added a section to introduce range-based for loops. Moved the section on C-style arrays to a dedicated lesson.
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.