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
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:
1 2 3 |
#include <array> std::array<int, 3> myArray; // declare an integer array with length 3 |
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 list initialization:
1 2 |
std::array<int, 5> myArray = { 9, 7, 5, 3, 1 }; // initializer list std::array<int, 5> myArray2 { 9, 7, 5, 3, 1 }; // list initialization |
Unlike built-in fixed arrays, with std::array you can not omit the array length when providing an initializer:
1 2 |
std::array<int, > myArray { 9, 7, 5, 3, 1 }; // illegal, array length must be provided std::array<int> myArray { 9, 7, 5, 3, 1 }; // illegal, array length must be provided |
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.
1 2 |
std::array myArray { 9, 7, 5, 3, 1 }; // The type is deduced to std::array<int, 5> std::array myArray { 9.7, 7.31 }; // The type is deduced to std::array<double, 2> |
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.
1 2 3 4 5 |
// std::array myArray { 9, 7, 5, 3, 1 }; // Since C++17 std::array<int, 5> myArray { 9, 7, 5, 3, 1 }; // Before C++17 // std::array myArray { 9.7, 7.31 }; // Since C++17 std::array<double, 2> myArray { 9.7, 7.31 }; // Before C++17 |
Since C++20, it is possible to specify the element type but omit the array length. This makes creation of std::array
a little more like creation of C-style arrays. To create an array with a specific type and deduced size, we use the std::to_array
function:
1 2 3 |
auto myArray1 { std::to_array<int, 5>({ 9, 7, 5, 3, 1 }) }; // Specify type and size auto myArray2 { std::to_array<int>({ 9, 7, 5, 3, 1 }) }; // Specify type only, deduce size auto myArray3 { std::to_array({ 9, 7, 5, 3, 1 }) }; // Deduce type and size |
Unfortunately, std::to_array
is more expensive than creating a std::array
directly, because it actually copies all elements from a C-style array to a std::array
. For this reason, std::to_array
should be avoided when the array is created many times, eg. in a loop.
You can also assign values to the array using an initializer list
1 2 3 4 |
std::array<int, 5> myArray; myArray = { 0, 1, 2, 3, 4 }; // okay myArray = { 9, 8, 7 }; // okay, elements 3 and 4 are set to zero! myArray = { 0, 1, 2, 3, 4, 5 }; // not allowed, too many elements in initializer list! |
Accessing std::array
values using the subscript operator works just like you would expect:
1 2 |
std::cout << myArray[1] << '\n'; myArray[2] = 6; |
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:
1 2 3 |
std::array myArray { 9, 7, 5, 3, 1 }; myArray.at(1) = 6; // array element 1 is valid, sets array element 1 to value 6 myArray.at(9) = 10; // array element 9 is invalid, will throw an error |
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
:
1 2 |
std::array myArray { 9.0, 7.2, 5.4, 3.6, 1.8 }; std::cout << "length: " << myArray.size() << '\n'; |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <array> #include <iostream> void printLength(const std::array<double, 5> &myArray) { std::cout << "length: " << myArray.size() << '\n'; } int main() { std::array myArray { 9.0, 7.2, 5.4, 3.6, 1.8 }; printLength(myArray); return 0; } |
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
:
1 2 3 4 |
std::array myArray{ 9, 7, 5, 3, 1 }; for (int element : myArray) std::cout << element << ' '; |
You can sort std::array
using std::sort
, which lives in the <algorithm> header:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <algorithm> // for std::sort #include <array> #include <iostream> int main() { std::array myArray { 7, 3, 1, 9, 5 }; std::sort(myArray.begin(), myArray.end()); // sort the array forwards // std::sort(myArray.rbegin(), myArray.rend()); // sort the array backwards for (int element : myArray) std::cout << element << ' '; std::cout << '\n'; return 0; } |
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?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> #include <array> int main() { std::array myArray { 7, 3, 1, 9, 5 }; // Iterate through the array and print the value of the elements for (int i{ 0 }; i < myArray.size(); ++i) std::cout << myArray[i] << ' '; std::cout << '\n'; return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <array> #include <iostream> int main() { std::array myArray { 7, 3, 1, 9, 5 }; // std::array<int, 5>::size_type is the return type of size()! for (std::array<int, 5>::size_type i{ 0 }; i < myArray.size(); ++i) std::cout << myArray[i] << ' '; std::cout << '\n'; return 0; } |
That's not very readable. Fortunately, std::array::size_type
is just an alias for std::size_t
, so we can use that instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <array> #include <cstddef> // std::size_t #include <iostream> int main() { std::array myArray { 7, 3, 1, 9, 5 }; for (std::size_t i{ 0 }; i < myArray.size(); ++i) std::cout << myArray[i] << ' '; std::cout << '\n'; return 0; } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <array> #include <iostream> int main() { std::array myArray { 7, 3, 1, 9, 5 }; // Print the array in reverse order. // We can use auto, because we're not initializing i with 0. // Bad: for (auto i{ myArray.size() - 1 }; i >= 0; --i) std::cout << myArray[i] << ' '; std::cout << '\n'; return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <array> #include <iostream> int main() { std::array myArray { 7, 3, 1, 9, 5 }; // Print the array in reverse order. for (auto i{ myArray.size() }; i-- > 0; ) std::cout << myArray[i] << ' '; std::cout << '\n'; return 0; } |
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.
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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <array> #include <iostream> struct House { int number{}; int stories{}; int roomsPerStory{}; }; int main() { std::array<House, 3> houses{}; houses[0] = { 13, 4, 30 }; houses[1] = { 14, 3, 10 }; houses[2] = { 15, 3, 40 }; for (const auto& house : houses) { std::cout << "House number " << house.number << " has " << (house.stories * house.roomsPerStory) << " rooms\n"; } return 0; } |
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.
1 2 3 4 5 6 |
// Doesn't work. std::array<House, 3> houses{ { 13, 4, 30 }, { 14, 3, 10 }, { 15, 3, 40 } }; |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct Array { int value[3]{}; }; int main() { Array array{ 11, 12, 13 }; return 0; } |
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 House
s, we get an error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
struct House { int number{}; int stories{}; int roomsPerStory{}; }; struct Array { // This is now an array of House House value[3]{}; }; int main() { // If we try to initialize the array, we get an error. Array houses{ { 13, 4, 30 }, { 14, 3, 10 }, { 15, 3, 40 } }; return 0; } |
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:
1 2 3 4 5 6 |
// This is wrong Array houses{ { 13, 4, 30 }, // value[0] { 14, 3, 10 }, // value[1] { 15, 3, 40 } // value[2] }; |
The compiler tries to initialize the Array
like this:
1 2 3 4 5 |
Array houses{ { 13, 4, 30 }, // value { 14, 3, 10 }, // ??? { 15, 3, 40 } // ??? }; |
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:
1 2 3 4 5 6 7 8 9 |
struct S { int arr[3]{}; int i{}; }; // These two do the same S s1{ { 1, 2, 3 }, 4 }; S s2{ 1, 2, 3, 4 }; |
To initialize all houses, we need to do so in the first pair of braces.
1 2 3 |
Array houses{ { 13, 4, 30, 14, 3, 10, 15, 3, 40 }, // value }; |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> struct House { int number{}; int stories{}; int roomsPerStory{}; }; struct Array { House value[3]{}; }; int main() { // With braces, this works. Array houses{ { { 13, 4, 30 }, { 14, 3, 10 }, { 15, 3, 40 } } }; for (const auto& house : houses.value) { std::cout << "House number " << house.number << " has " << (house.stories * house.roomsPerStory) << " rooms\n"; } return 0; } |
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.
![]() |
![]() |
![]() |
Hey can you please help me with this code. It is not printing the sorted array.
Unless you enter exactly 100 elements, your program causes undefined behavior because `a` is uninitialized.
Ohk thanks
What's the difference between these 2? Meaning and usage wise.I checked cppreference, it says the ={} is copy-list-initialization. Thank you for your time!
If I do this:
auto would become of type std::size_t?
If so, are there any disadvantages?
`iii` is type `std::size_t`.
You're calling a potentially expensive function when you don't need its return value.
If you want to get the type of something, you can use `decltype`
But since `std::array::size_type` is an alias for `std::size_t`, you can just use `std::size_t`.
Suggestion.
Could you add in a Syntax Template at the beginning?
std::array<T,N> arrayname{}; , where T is the Type, N is the size
This will help in understanding.
I'm just a bit confused with why we have a problem with the code you provided in the first example of "Manually indexing std::array via size_type"
I somewhat understood your explanation below it, but I don't really see how comparing unsigned vs signed types is an issue. I ran the above code and didn't get any errors or warnings. I guess I don't exactly understand how a signed/ unsigned mismatch is "bad" and I don't remember if it was ever explained in the other sections. Does it lead to undefined behavior?
This prints
i >= ui
Probably not what the programmer expected.
When a signed integer is compared to an unsigned integer, the signed integer gets converted to an unsigned integer (ie. it becomes potentially huge if it had a negative value).
gcc and clang warn for the above code with -Wsign-conversion
msvc warns for the above code with C4018
Ahh, okay, thank you!
I know my code is wrong. But explain why it doesn't compile. I can't understand the compiler's explanation of the error occurred
You're creating an array of `int` (Line 5), then you try to assign an `int*` to an `int` (Line 7).
You can't assign an `int*` to an `int`.
Oh yeah! Got it. Thanks ☺️
It took a while for me to understand the printing the array element in reverse order where we used postfix decrement. From lesson 5.4 I understand, in postfix operation, that first a copy of variable's r-value is made, then increment or decrement happens, this increment or decrement is not reflected on postfix operation statement, but they are reflected the subsequent operation.
So the first iteration of the above code on line 6, the value of i is 5; On statement `i-- > 0;` : i decremented by 1, but this decrementation is not reflected on this line that is why value of i is still 5 , and 5 is being compared with 0, this comparison is true hence operation enters into loop body, as the statements inside loop body is subsequent operation after decrement so here i's value is 4 so the first element of the array is printed. When i's value is 1, it gets decremented but 1 > 0 so inside of loop body i = 0 is used, and the last element of the array is printed. When i = 0, it gets decremented, here i is wraps around as size(array) returns unsigned integer, but 0 > 0 is false so the loop terminates.
I wanted to avoid unsigned integer(as it is potentially dangerous), postfix operation(as it involves copying), and wrapping around(potential infinite loop). So I coded like this to print the array's elements in reverse order:
but compiler complains when using the highest level of warning on line 19 as compiler expects the unsigned integer for indexing the `std::array`.
Here is the warning message:
So I changed the line 19 like this:
Now compiler is happy, still conversion from one type to another is computationally expensive.
Both `--i` and `i--` decrement `i`. The only different is their return value. `--i` returns the new value of `i`, whereas `i--` returns the old value of `i`.
The index type of `std::array` is `std::size_t`, not `uint`. The conversion is free as long as both types have the same size and binary representation, which is the case for `long unsigned int` and `long int` if the value is non-negative.
oh my.. the intro says that std::array exists because built-in fixed arrays are too confusing to use.. but it turns out std::array is even worse!.. you mentioned that with std::array you can use size() inside a function, but what's the use of it if you need to specify the size of the array in the function declaration? let's hope std::vector in the next lesson saves the day! xD
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?
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.
Typo: "If we add braces around each element of the array, the initialization is a lot easy to read." easy -> easier
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.
Example updated, thanks!