An Introduction to std::array

Following up on last week's article on std::vector, today we will be focusing on std::array. Where std::vector represented dynamically sized arrays, std::array is a container that represents arrays of fixed size. std::array lends itself nicely to use in embedded systems, as memory can be statically allocated at compile-time.

std::array Overview

In order to utilize std::array, you will need to include the array header:

#include <array>

std::array is a header-only implementation, which means that once you have a C++ runtime set up for your target system you will get this feature for free.

std::array provides many benefits over built-in arrays, such as preventing automatic decay into a pointer, maintaining the array size, providing bounds checking, and allowing the use of C++ container operations.

As mentioned above, std::array is a templated class that represents fixed-size arrays. The size of a std::array is known at compile time, and the underlying buffer is stored within the std::array object itself. These two facts are very useful for embedded systems programmers, as they can control buffer sizes and storage location during compilation time. Avoiding the need for dynamic memory allocation saves computational cycles and reduces memory fragmentation. If you declare the object on the stack, the array itself will be on the stack. If you put it in a global scope, it will be placed into global static storage.

Unlike std::vector, which requires 24 bytes of overhead, no overhead is needed for a std::array.

Creating a std::array

The std::array container is prototyped on two elements: the type that you want to be stored and the size of the array. std::array containers of different sizes are viewed as different types by the compiler.

//Declares an array of 10 ints. Size is always required
std::array<int, 10> a1;

//Declare and initialize with an initializer list
std::array<int, 5> a2 = {-1, 1, 3, 2, 0};

You can make new arrays via copy:

//Making a new array via copy
auto a3 = a2;

//This works too:
auto a4(a2);

And you can also copy arrays of the same size by using the = operator:

//Assign a2 to a3's values:
a2 = a3;

// But you can only use the '=' operator on arrays of equivalent size.
//Error:
//a1 = a2; //<[...],10> vs <[...],5>! invalid

At least you don't have to worry about remembering memcpy argument order or writing past the end of your buffer!

Accessing Data

While the size of a std::array is fixed at compile time, the contents of a std::array can be modified during runtime. The familiar [] operator can be used to access specific elements:

//Assigning values works as expected
a3[0] = -2;

However, as with std::vector, the [] operator does not use bounds checking. If you want to access an element with bound checks enabled, use the at() function.

std::cout << "a2.at(4): " << a2.at(4) << std::endl;

// Bounds checking can generate exceptions. Try:
//auto b = a2.at(10);

You can also access the front() and back() member functions to get the members at the beginning & end of the array.

data()

Like std::vector, std::array doesn't implicitly decay into a raw pointer. If you want to use the underlying std::array pointer, you must use the data() member function.

For example, let's assume you are using an API with a C-style buffer interface:

void carr_func(int * arr, size_t size)
{
    std::cout << "carr_func - arr: " << arr << std::endl;
}

If you tried to pass a std::array for the first argument, you would generate a compiler error.

../../array.cpp:44:2: error: no matching function for call to 'carr_func'
        carr_func(a2);
        ^~~~~~~~~
../../array.cpp:4:6: note: candidate function not viable: no known conversion
      from 'std::array<int, 5>' to 'int *' for 1st argument
void carr_func(int * arr)

Instead you need to use the data() member:

//Error:
//carr_func(a2, a2.size());

//OK:
carr_func(a2.data(), a2.size());

size() and max_size()

You can access the size of a std::array using the size() member function. max_size() is also valid for std::array. However, since the size of a std::array is constant, max_size will always be equal to size.

empty()

std::array has a specific use for the empty() member function which differs from other containers: it only returns True if the array size is 0:

std::array<int, 0> a_empty;

The underlying empty() operation checks if the container has no elements (begin() == end()). Since std::array is statically sized, this condition is only hit when you have a zero-length array.

Container Operations

Since std::array is a container class and provides the basic container interfaces. Since a std::array cannot grow or shrink, all functionality related to resizing or remembering a current position has been removed (e.g. push_back).

However, you can still use a std::array with functions that are written to operate on container classes, such as std::sort.

std::sort(a1.begin(), a1.end());

Also worth noting - unlike built-in arrays (which decay into a pointer), a std::array container can be passed by value into a function.

Putting it All Together

Example code for std::array can be found in the embedded-resources Github repository.

Further Reading