In lesson 16.1 -- Introduction to containers and arrays, we introduced containers and arrays. To summarize:
- Containers provide storage for a collection of unnamed objects (called elements).
- Arrays allocate their elements contiguously in memory, and allow fast, direct access to any element via subscripting.
- C++ has three different array types that are commonly used:
std::vector
,std::array
, and C-style arrays.
In lesson 16.10 -- std::vector resizing and capacity, we mentioned that arrays fall into two categories:
- Fixed-size arrays (also called fixed-length arrays) require that the length of the array be known at the point of instantiation, and that length cannot be changed afterward. C-style arrays and
std::array
are both fixed-size arrays. - Dynamic arrays can be resized at runtime.
std::vector
is a dynamic array.
In the previous chapter, we focused on std::vector
, as it is fast, comparatively easy to use, and versatile. This makes it our go-to type when we need an array container.
So why not use dynamic arrays for everything?
Dynamic arrays are powerful and convenient, but like everything in life, they make some tradeoffs for the benefits they offer.
std::vector
is slightly less performant than the fixed-size arrays. In most cases you probably won’t notice the difference (unless you’re writing sloppy code that causes lots of inadvertent reallocations).std::vector
only supportsconstexpr
in very limited contexts.
In modern C++, it is really this latter point that’s significant. Constexpr arrays offer the ability to write code that is more robust, and can also be optimized more highly by the compiler. Whenever we can use a constexpr array, we should -- and if we need a constexpr array, std::array
is the container class we should be using.
Best practice
Use std::array
for constexpr arrays, and std::vector
for non-constexpr arrays.
Defining a std::array
std::array
is defined in the <array> header. It is designed to work similarly to std::vector
, and as you’ll see, there are more similarities than differences between the two.
One difference is in how we declare a std::array
:
Our std::array
declaration has two template arguments. The first (int
) is a type template argument defining the type of the array element. The second (5
) is an integral non-type template argument defining the array length.
Related content
We cover non-type template parameters in lesson 11.9 -- Non-type template parameters.
The length of a std::array
must be a constant expression
Unlike a std::vector
, which can be resized at runtime, the length of a std::array
must be a constant expression. Most often, the value provided for the length will be an integer literal, constexpr variable, or an unscoped enumerator.
Note that non-const variables and runtime constants cannot be used for the length:
Warning
Perhaps surprisingly, a std::array
can be defined with a length of 0:
A zero-length std::array
is a special-case class that has no data. As such, calling any member function that accesses the data of a zero-length std::array
(including operator[]
) will produce undefined behavior.
You can test whether a std::array
is zero-length using the empty()
member function, which returns true
if the array is zero-length and false
otherwise.
Aggregate initialization of a std::array
Perhaps surprisingly, std::array
is an aggregate. This means it has no constructors, and instead is initialized using aggregate initialization. As a quick recap, aggregate initialization allows us to directly initialize the members of aggregates. To do this, we provide an initializer list, which is a brace-enclosed list of comma-separated initialization values.
Related content
We covered aggregate initialization for structs in lesson 13.8 -- Struct aggregate initialization.
Each of these initialization forms initializes the array members in sequence, starting with element 0.
If a std::array
is defined without an initializer, the elements will be default initialized. In most cases, this will result in elements being left uninitialized.
Because we generally want our elements to be initialized, std::array
should be value initialized (using empty braces) when defined with no initializers.
If more initializers are provided in an initializer list than the defined array length, the compiler will error. If fewer initializers are provided in an initializer list than the defined array length, the remaining elements without initializers are value initialized:
Const and constexpr std::array
A std::array
can be const:
Even though the elements of a const std::array
are not explicitly marked as const, they are still treated as const (because the whole array is const).
std::array
also has full support for constexpr:
This support for constexpr is the key reason to use std::array
.
Best practice
Define your std::array
as constexpr whenever possible. If your std::array
is not constexpr, consider using a std::vector
instead.
Class template argument deduction (CTAD) for std::array
C++17
Using CTAD (class template argument deduction) in C++17, we can have the compiler deduce both the element type and the array length of a std::array
from a list of initializers:
We favor this syntax whenever practical. If your compiler is not C++17 capable, you’ll need to explicitly provide the type and length template arguments.
Best practice
Use class template argument deduction (CTAD) to have the compiler deduce the type and length of a std::array
from its initializers.
CTAD does not support partial omission of template arguments (as of C++23), so there is no way to use a core language feature to omit just the length or just the type of a std::array
:
Omitting just the array length using std::to_array
C++20
However, TAD (template argument deduction, used for function template resolution) does support partial omission of template arguments. Since C++20, it is possible to omit the array length of a std::array
by using the std::to_array
helper function:
Unfortunately, using std::to_array
is more expensive than creating a std::array
directly, because it involves creation of a temporary std::array
that is then used to copy initialize our desired std::array
. For this reason, std::to_array
should only be used in cases where the type can’t be effectively determined from the initializers, and should be avoided when an array is created many times (e.g. inside a loop).
For example, because there is no way to specify a literal of type short
, you could use the following to create an std::array
of short
values (without having to explicitly specify the length of the std::array
):
Accessing array elements using operator[]
Just like a std::vector
, the most common way to access elements of a std::array
is by using the subscript operator (operator[]
):
As a reminder, operator[]
does not do bounds checking. If an invalid index is provided, undefined behavior will result.
We’ll discuss a few other ways to index a std::array
in the next lesson.
Quiz time
Question #1
What type of initialization does std::array
use?
Why should you explicitly value-initialize a std::array
if you are not providing initialization values?
Question #2
Define a std::array
that will hold the high temperature for each day of the year (to the nearest tenth of a degree).
Question #3
Initialize a std::array
with the following values: ‘h’, ‘e’, ‘l’, ‘l’, ‘o’. Print the value of the element with index 1.