Preferring [[std::array]] and [[std::vector]] over raw arrays buys you three things: the size travels with the data, the container knows how many elements it holds, and the interface is uniform enough that algorithms written for one work on the other. The raw array is not wrong, it is just missing the parts you would end up writing by hand anyway.
std::array: fixed size, full interface
std::array wraps a raw array in a class. The storage is identical, so performance is identical, but the type now carries its size and offers the same interface as every other standard container.
#include
#include
int main() {
std::array ratings = {4, 5, 3, 5};
std::cout << "count: " << ratings.size() << '\n';
std::cout << "first: " << ratings.front() << '\n';
std::cout << "last: " << ratings.back() << '\n';
std::cout << "element 2: " << ratings[2] << '\n';
ratings[2] = 4;
std::cout << "after edit: ";
for (int r : ratings) {
std::cout << r << ' ';
}
std::cout << '\n';
return 0;
}The output shows the container behaving like an array with methods attached:
count: 4
first: 4
last: 5
element 2: 3
after edit: 4 5 4 5The important gain is not the convenience of .front() and .back(). It is that std::array<int, 4> and std::array<int, 5> are different types, so passing the wrong one to a function is a compile error instead of a runtime bug.
std::vector: size decided at runtime
std::vector is a dynamic array. It owns its memory, grows when you add to it, and frees it when it goes out of scope.
#include
#include
int main() {
std::vector ratings;
for (int i = 0; i < 5; ++i) {
ratings.push_back(3 + (i % 3));
}
std::cout << "count: " << ratings.size() << '\n';
std::cout << "first: " << ratings.front() << '\n';
ratings[0] = 5;
ratings.at(0) = 5;
std::cout << "after edit: ";
for (int r : ratings) {
std::cout << r << ' ';
}
std::cout << '\n';
return 0;
}Note the difference between ratings[0] and ratings.at(0). The subscript operator does not check the index. The .at() member does, and throws an exception if the index is out of range. The output shows the five ratings after the edit:
count: 5
first: 3
after edit: 5 4 5 3 4Comparing the two side by side
The table below is the whole argument in one place.
- Size —
std::arrayis fixed at compile time;std::vectorchanges at runtime. - Memory —
std::arrayis embedded in the object;std::vectorowns a separate allocation. - Bounds —
std::arrayis checked by the compiler when the type is part of a signature;std::vectorcan also be checked at runtime with.at(). - Copy cost — copying an
std::arraycopies every element; copying astd::vectorcopies the elements too, but a vector can be moved in constant time. - Interface — both provide
.size(),.begin(),.end(),.empty(),.front(),.back()and range-basedfor. - Algorithms — the standard algorithms accept either, because both satisfy the same requirements.
When to use which
Reach for std::array when the number of elements is fixed and known when you write the code: a small lookup table, a coordinate pair, a fixed-size buffer. Reach for std::vector when the number of elements is only known at runtime, or when it changes often.
There is a third option worth knowing: std::span, added in C++20. It is a non-owning view of a contiguous sequence that knows its length, which is exactly what a raw array pointer plus count wants to be. For C++17 code, std::array and std::vector cover the same ground.
Initialising a vector
A std::vector has several useful ways to be created, and they differ in what they do to the size and the contents.
#include
#include
int main() {
std::vector empty; // no elements
std::vector zeros(5); // five elements, all 0
std::vector repeated(5, 2); // five elements, all 2
std::vector from_list = {4, 5, 3}; // three elements from a list
std::vector copied = from_list; // a copy of the three
std::cout << "empty: " << empty.size() << '\n';
std::cout << "zeros: " << zeros[0] << '\n';
std::cout << "repeated: " << repeated[4] << '\n';
std::cout << "copied: " << copied.size() << '\n';
return 0;
}The output confirms each construction:
empty: 0
zeros: 0
repeated: 2
copied: 3Two of these deserve special mention. A vector constructed with a *count* such as std::vector<int> zeros(5) creates five zero-valued elements, which is different from std::vector<int> zeros{5} — with braces, the 5 is an element value, so you get one element holding the number 5. And std::vector never shrinks when you assign it a smaller list; it keeps its capacity and simply holds fewer elements.
Using one interface with both
The standard algorithms work on any container that provides the right iterators, so the same code handles an array and a vector.
#include
#include
#include
#include
#include
template
void report(const Shelf& shelf) {
std::cout << "count: " << shelf.size() << '\n';
std::cout << "total: " << std::accumulate(shelf.begin(), shelf.end(), 0) << '\n';
std::cout << "best: " << *std::max_element(shelf.begin(), shelf.end()) << '\n';
}
int main() {
std::array fixed_shelf = {4, 5, 3};
std::vector growing_shelf = {4, 5, 3, 2, 5};
report(fixed_shelf);
report(growing_shelf);
return 0;
}One template handles both containers, and the output confirms it:
count: 3
total: 12
best: 5
count: 5
total: 19
best: 5Quick reference
std::arrayhas a compile-time size, no allocation, and the same memory layout as a raw array.std::vectorhas a runtime size, owns its memory and grows automatically.- Both give you
.size(),.begin(),.end(),.front()and.back(). .at()checks the index and throws; the subscript operator does not check.- Both satisfy the standard algorithms' requirements, so one function can handle either.
- Use
std::arrayfor a fixed count,std::vectorfor a count you only learn at runtime.
Try it yourself: write the same "average rating" program twice, once with a [[std::array]] of five elements and once with a [[std::vector]] you fill at runtime. Then write one template function that reports the count, total and best rating for either, and call it with both. Finally, replace the subscript with [[.at()]] and pass an index that is one too high to see the exception.