Static Arrays using std::array

An introduction to static arrays using std::array - an object that can store a collection of other objects
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

Inevitably, 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:

Creating a 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:

Setting the Size of a 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

Element Access using the [] Operator

We 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

The Subscript Operator

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, $x_1$ would generally refer to element $1$ of some collection $x$

Element Access using the at() method

There 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.

Using the std::size_t type

There 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(){
  // ...
}

Iteration using a for Loop

A 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.

Using the size() Method

The 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,

Iteration using a Range-Based For Loop

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.

Updating Objects in Arrays

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,

Storing More Complex Types

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

Multidimensional Arrays

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.

Type Aliases

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:

Array Pointers and References

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];
}

Summary

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.
  • We learned how to declare, initialize, and access elements in std::array using various methods, including subscript notation and at() for bounds-checked access.
  • The lesson covered the importance of std::size_t for indexing and the use of size() method to make code more adaptable and less error-prone.
  • We discussed iterating over std::array elements using traditional for loops and range-based for loops.
  • The versatility of std::array was highlighted through examples showing it can store complex types, including multidimensional arrays.

Was this lesson useful?

Next Lesson

C-Style Arrays

A detailed guide to working with classic C-style arrays within C++, and why we should avoid them where possible
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Arrays and Linked Lists
Next Lesson

C-Style Arrays

A detailed guide to working with classic C-style arrays within C++, and why we should avoid them where possible
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved