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, to form a party. This is where arrays can help us. Arrays are objects designed to store a collection of other objects.
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.
Note: this lesson introduces static arrays, which have a fixed size that must be known at compile time. For most cases, a dynamic array is more useful, as it can grow and shrink as needed at run time. We covered dynamic arrays in our beginner course:
std::array
There are hundreds of implementations of arrays in C++ that we can use, and we can even create our own once we learn more advanced topics.
The standard library’s implementation of static arrays is called std::array
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;
The number of objects an array contains - 5 in the previous example - is sometimes referred to as its size or its length.
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. The compiler can infer this based on the initial values we provide.
As such, the following code is equivalent to the previous:
#include <array>
std::array MyArray { 1, 2, 3, 4, 5 };
To do this inference, the compiler is using Class Template Argument Deduction (CTAD), which we covered in our earlier lessons on templates:
std::array
A 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 the following 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:
#include <array>
int GetSize() { // no longer constexpr
return 2 + 3;
}
std::array<int, GetSize()> MyArray;
invalid template argument for 'std::array', expected compile-time constant expression
[]
OperatorWe 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, indexing is zero-based. That means we start counting from 0
. Therefore, 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 0
, 1
, and 2
in order:
First
Second
Third
We also have the front()
and back()
methods, as an alternative way to access the first and last elements respectively:
#include <array>
#include <iostream>
int main(){
using std::cout, std::array;
array MyArray{"First", "Second", "Third"};
// Log out the element at index 0
cout << "Front: " << MyArray.front();
// Log out the element at index [size - 1]
cout << "\nBack: " << MyArray.back();
}
Front: First
Back: Third
When a type implements the []
operator, that usually denotes access to some member of a collection.
For this reason, it is called the subscript operator, as the subscript notation is used in maths to denote a similar idea. For example, would generally refer to element of some collection
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.
Specifically, it will throw a std::out_of_range
exception:
#include <array>
#include <iostream>
int main() {
using std::array;
array MyArray{"First", "Second", "Third"};
try {
MyArray.at(3);
} catch (std::out_of_range& e) {
std::cout << e.what();
}
}
invalid array<T, N> subscript
However, in most cases, we should not use at()
. The additional check performed by at()
has a performance cost, and it 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.
#include <array>
#include <iostream>
int main() {
using std::array;
array MyArray{"First", "Second", "Third"};
MyArray[3];
}
array subscript out of range
example.exe (process 52600) exited with code 3.
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(){
// ...
}
for
LoopA common task we have when working with arrays is to loop over every element in them, 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 (size_t i{0}; i < 3; ++i) { // see below
std::cout << MyArray[i] << ", ";
}
}
1, 2, 3,
Rather than having the array’s size of 3
included in our loop header, we can make our code a little smarter and more resilient to code changes.
size()
MethodThe previous example uses i < 3
to determine when our loop should end, as we know our array has a size of 3
.
However, if we later update our code to change the size of our array, we would need to find and update everywhere we were assuming the size to be 3
, or we would have a bug.
Additionally, we don’t always know the array size - for example, we might be writing a template function that can be instantiated with arrays of different sizes.
We can make our code a little smarter - std::array
has a size()
method that, predictably, returns the size of the array:
#include <iostream>
#include <array>
int main(){
std::array MyArray{1, 2, 3};
std::cout << "Size: " << MyArray.size();
}
Size: 3
We can update our loop to use this method:
#include <iostream>
#include <array>
int main(){
std::array MyArray{1, 2, 3};
for (size_t i{0}; i < MyArray.size(); ++i) {
std::cout << MyArray[i] << ", ";
}
}
1, 2, 3,
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 << ", ";
}
}
1, 2, 3,
We cover ranges and range-based for loops later in the chapter.
Once we’ve accessed an object in an array - for example, using the []
or at()
method - we can use it as normal.
For example, we can apply operators to it, or send it to a function:
#include <array>
#include <iostream>
void Double(int& x) { x *= 2; }
int main() {
std::array MyArray{1, 2, 3};
MyArray.front()++;
MyArray[1] += 2;
Double(MyArray.back());
for (int i : MyArray) {
std::cout << i << ", ";
}
}
2, 4, 6,
We can replace an object at an index entirely using the assignment operator, =
:
#include <array>
#include <iostream>
void Set(int& x, int y) { x = 300; }
int main() {
std::array MyArray{1, 2, 3};
MyArray.front() = 100;
MyArray[1] = 200;
Set(MyArray.back(), 300);
for (int i : MyArray) {
std::cout << i << ", ";
}
}
100, 200, 300,
Our 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;
}
Where the objects stored in our array have member functions, we can access them in the usual way:
#include <array>
#include <iostream>
class Character {
public:
void SetHealth(int Health) {
mHealth = Health;
}
int GetHealth() { return mHealth; }
private:
int mHealth{100};
};
int main() {
std::array<Character, 5> Party;
Party[0] = Character{};
std::cout << "Health: "
<< Party[0].GetHealth();
Party[0].SetHealth(200);
std::cout << "\nHealth: "
<< Party[0].GetHealth();
}
Health: 100
Health: 200
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]};
Later in the course, we cover dedicated types to represent multi-dimensional arrays, but nesting arrays in this way is still commonly used.
Often, C++ types can become very complex. This can make our code hard to read or frustrating to write if we constantly need to repeat a huge type name.
We can use a using
statement to create a simpler alias for our types. Here, we alias our complex nested 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 earlier in the course:
Like any other object, we can 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];
}
In this lesson, we explored the use of std::array
in C++, covering its fundamental properties and usage. We've gained a solid understanding of how to effectively utilize std::array
, including:
std::array
provides a safe, fixed-size array alternative to dynamic arrays such as std::vector
.std::array
using various methods, including subscript notation and at()
for bounds-checked access.std::size_t
for indexing and the use of size()
method to make code more adaptable and less error-prone.std::array
elements using traditional for loops and range-based for loops.std::array
was highlighted through examples showing it can store complex types, including multidimensional arrays.std::array
An introduction to static arrays using std::array
- an object that can store a collection of other objects
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.