Search

6.15 — An introduction to std::array

In previous lessons, we’ve talked at length about fixed and dynamic arrays. Although both are built right into the C++ language, they both have downsides: Fixed arrays decay into pointers, losing the array length information when they do, and dynamic arrays have messy deallocation issues and are challenging to resize without error.

To address these issues, the C++ standard library includes functionality that makes array management easier, std::array and std::vector. We’ll examine std::array in this lesson, and std::vector in the next.

An introduction to std::array

Introduced in C++11, std::array provides fixed array functionality that won’t decay when passed into a function. std::array is defined in the <array> header, inside the std namespace.

Declaring a std::array variable is easy:

Just like the native implementation of fixed arrays, the length of a std::array must be known at compile time.

std::array can be initialized using initializer lists or uniform initialization:

Unlike built-in fixed arrays, with std::array you can not omit the array length when providing an initializer:

However, since C++17, it is allowed to omit the type and size. They can only be omitted together, but not one or the other, and only if the array is explicitly initialized.

We favor this syntax rather than typing out the type and size at the declaration. If your compiler is not C++17 capable, you need to use the explicit syntax instead.

You can also assign values to the array using an initializer list

Accessing std::array values using the subscript operator works just like you would expect:

Just like built-in fixed arrays, the subscript operator does not do any bounds-checking. If an invalid index is provided, bad things will probably happen.

std::array supports a second form of array element access (the at() function) that does bounds checking:

In the above example, the call to myArray.at(1) checks to ensure the index 1 is valid, and because it is, it returns a reference to array element 1. We then assign the value of 6 to this. However, the call to myArray.at(9) fails because array element 9 is out of bounds for the array. Instead of returning a reference, the at() function throws an error that terminates the program (note: It’s actually throwing an exception of type std::out_of_range -- we cover exceptions in chapter 14). Because it does bounds checking, at() is slower (but safer) than operator[].

std::array will clean up after itself when it goes out of scope, so there’s no need to do any kind of manual cleanup.

Size and sorting

The size() function can be used to retrieve the length of the std::array:

This prints:

length: 5

Because std::array doesn’t decay to a pointer when passed to a function, the size() function will work even if you call it from within a function:

This also prints:

length: 5

Note that the standard library uses the term “size” to mean the array length — do not get this confused with the results of sizeof() on a native fixed array, which returns the actual size of the array in memory (the size of an element multiplied by the array length). Yes, this nomenclature is inconsistent.

Also note that we passed std::array by (const) reference. This is to prevent the compiler from making a copy of the std::array when the std::array was passed to the function (for performance reasons).

Rule

Always pass std::array by reference or const reference

Because the length is always known, range-based for-loops work with std::array:

You can sort std::array using std::sort, which lives in the <algorithm> header:

This prints:

1 3 5 7 9

The sorting function uses iterators, which is a concept we haven’t covered yet, so for now you can treat the parameters to std::sort() as a bit of magic. We’ll explain them later.

Manually indexing std::array via size_type

Pop quiz: What’s wrong with the following code?

The answer is that there’s a likely signed/unsigned mismatch in this code! Due to a curious decision, the size() function and array index parameter to operator[] use a type called size_type, which is defined by the C++ standard as an unsigned integral type. Our loop counter/index (variable i) is a signed int. Therefore both the comparison i < myArray.size() and the array index myArray[i] have type mismatches.

Interestingly enough, size_type isn't a global type (like int or std::size_t). Rather, it's defined inside the definition of std::array (C++ allows nested types). This means when we want to use size_type, we have to prefix it with the full array type (think of std::array acting as a namespace in this regard). In our above example, the fully-prefixed type of "size_type" is std::array<int, 5>::size_type!

Therefore, the correct way to write the above code is as follows:

That's not very readable. Fortunately, std::array::size_type is just an alias for std::size_t, so we can use that instead.

A better solution is to avoid manual indexing of std::array in the first place. Instead, use range-based for-loops (or iterators) if possible.

Keep in mind that unsigned integers wrap around when you reach their limits. A common mistake is to decrement an index that is 0 already, causing a wrap-around to the maximum value. You saw this in the lesson about for-loops, but let's repeat.

This is an infinite loop, producing undefined behavior once i wraps around. There are two issues here. If `myArray` is empty, ie. size() returns 0 (which is possible with std::array), myArray.size() - 1 wraps around. The other issue occurs no matter how many elements there are. i >= 0 is always true, because unsigned integers cannot be less than 0.

A working reverse for-loop for unsigned integers takes an odd shape:

Suddenly we decrement the index in the condition, and we use the postfix -- operator. The condition runs before every iteration, including the first. In the first iteration, i is myArray.size() - 1, because i was decremented in the condition. When i is 0 and about to wrap around, the condition is no longer true and the loop stops. i actually wraps around when we do i-- for the last time, but it's not used afterwards.

Array of struct

Of course std::array isn't limited to numbers as elements. Every type that can be used in a regular array can be used in a std::array.

Output

House number 13 has 120 rooms
House number 14 has 30 rooms
House number 15 has 120 rooms

However, things get a little weird when we try to initialize the array.

Although we can initialize std::array like this if its elements are simple types, like int or std::string, it doesn't work with types that need multiple values to be created. Let's have a look at why this is the case.

std::array is an aggregate type, just like House. There is no special function for the creation of a std::array. Rather, its internal array gets initialized like any other member variable of a struct. To make this easier to understand, we'll implement a simple array type ourselves.

As of now, we can't do this without having to access the value member. You'll learn how to get around that later. This doesn't affect the issue we're observing.

As expected, this works. So does std::array if we use it with int elements. When we instantiate a struct, we can initialize all of its members. If we try to create an Array of Houses, we get an error.

When we use braces inside of the initialization, the compiler will try to initialize one member of the struct for each pair of braces. Rather than initializing the Array like this:

The compiler tries to initialize the Array like this:

The first pair of inner braces initializes value, because value is the first member of Array. Without the other two pairs of braces, there would be one house with number 13, 4 stories, and 30 rooms per story.

A reminder

Braces can be omitted during aggregate initialization:

To initialize all houses, we need to do so in the first pair of braces.

This works, but it's very confusing. So confusing that your compiler might even warn you about it. If we add braces around each element of the array, the initialization is a lot easier to read.

This is why you'll see an extra pair of braces in initializations of std::array.

Summary

std::array is a great replacement for built-in fixed arrays. It's efficient, in that it doesn’t use any more memory than built-in fixed arrays. The only real downside of a std::array over a built-in fixed array is a slightly more awkward syntax, that you have to explicitly specify the array length (the compiler won’t calculate it for you from the initializer, unless you also omit the type, which isn't always possible), and the signed/unsigned issues with size and indexing. But those are comparatively minor quibbles — we recommend using std::array over built-in fixed arrays for any non-trivial array use.


6.16 -- An introduction to std::vector
Index
6.14 -- Pointers to pointers and dynamic multidimensional arrays

250 comments to 6.15 — An introduction to std::array

  • Galih

    Since this doesn't work

    How about something like this

    This seems work, but is it acceptable in that is it hard to read? Or it breaks down in some situation?

    • nascardriver

      The only things I don't like about it is that you're using `i`'s value _after_ it has wrapped around, and then have to to duplicate `myArray.size()`. Even then, your code shouldn't fail, because the maximum value `i` could have is always greater-than or equal-to `myArray.size()`, so the loop will terminate.

  • Martin

    Typo: "If we add braces around each element of the array, the initialization is a lot easy to read." easy -> easier

  • Martin

    I'd suggest to add "since C++17" and "prior to C++17" comments to

    in order to make the point of these examples clearer.

Leave a Comment

Put all code inside code tags: [code]your code here[/code]