std::array solves numerous issues which C-style arrays have. It's not about performance for the most part; it's about correctness and convenience. Here is the list of issues with C-style arrays that std::array solves.
1. Arrays cannot be returned from functions
This one is particularly annoying. Especially for tasks like initializing look-up tables, returning arrays is useful:
constexpr auto lookup = [] {
std::array<int, 128> result;
// compute the contents
return result;
}();
2. The hypothetical syntax for returning arrays is cursed
int get_array()[10];
auto get_array() -> int[10]; // workaround: trailing return types
According to the C declaration syntax, this is how a function get_array which returns an array of 10 ints would be declared.
This syntax is very surprising, and one of the reasons why it's very unlikely to ever be standardized in C.
3. Arrays cannot be assigned
int arr[] = {0, 1, 2, 3};
arr = something_else; // ill-formed
Assigning arrays is fairly common for small arrays, such as pairs or triples.
This feels particularly inconsistent because the syntax = can be used for initialization.
4. Arrays cannot be initialized to other arrays
int data[] = {0, 1};
int copy[] = data;
This is yet another restriction which feels arbitrary. What makes it even worse is that copy-initialization is valid for arrays in principle, just not using another array instead of a braced-init-list.
5. Comparison of arrays with == is a common mistake
int arr[] = {0, 1, 2, 3};
arr == arr; // true, but doesn't compare contents, but is pointer comparison
This is a common mistake, which is why array comparison was deprecated in C++23 and is likely going to be removed in C++26.
See also Why does == equality comparison between arrays not work?
6. Arrays decay to pointers, giving them surprising semantics
char arr[] = "hello ";
if (arr); // always true, even if array contains an empty string
+arr; // OK, but what does it mean to apply unary plus to an array?!
arr + 1; // OK, but result is not "hello 1", it is "ello "
The list goes on and on.
The fact that arrays decay to pointers often leads to counter-intuitive behavior.
Most of this behavior is not yet deprecated.
7. Arrays might be variable length arrays (VLAs)
int size = rand();
int vla[size]; // could be OK if the compiler supports VLAs as an extension
std::array<int, size> arr; // error, as expected
Without compiler warnings (-Wvla for GCC/clang), we can inadvertently create a VLA.
This makes code non-portable because not every compiler has support for VLAs.
8. Array type adjustment in function parameters is misleading
void foo(int arr[4]) { // equivalent to accepting a parameter of type int*
sizeof(arr) / sizeof(arr[0]); // = sizeof(void*) / sizeof(int), most likely 2
}
void foo(std::array<int, 4> arr) {
arr.size(); // 4, correct
}
Parameters of array type in function parameters are adjusted to pointer types.
This means that sizeof(arr) doesn't work properly, and the provided size [4] is actually meaningless.
This is very surprising, and using sizeof in conjunction with pointers when arrays were expected is one of the most common beginner mistakes in C.
9. Array sizes can't be deduced from function parameters
template <std::size_t N>
void foo(int arr[N]); // N can't be deduced from the array parameter
template <std::size_t N>
void foo(int (&arr)[N]); // workaround with complicated syntax
This issue is a consequence of the type adjustment to pointers in the prior point. There is no array type in the parameter from which N could be deduced. A workaround is necessary, but this workaround is not pretty.
10. Arrays lead to special cases in generic code
Due to all the aforementioned irregularities, arrays require special cases in generic code.
To name some examples:
std::swap must have an overload for arrays because arrays are immovable
- arrays aren't classes, so lots of free functions like
std::size, std::begin, std::empty, etc. are necessary that have a special case for arrays
Any developer who writes a library which contains range.begin() instead of std::begin(range) has to fear their code breaking if a user uses C-style arrays.
Of course, using std::array doesn't solve the underlying issue, but it means that you never suffer from a library developer having made the false assumption that .begin() is always valid.
Furthermore, these countless special cases still don't cover everything. You can use std::swap to do an element-wise swap of two arrays, but you cannot use std::exchange to exchange arrays element-wise (because arrays cannot be returned from functions).
11. Arrays may have poor performance due to aliasing
It is easy to inadvertently write code which has performance penalties caused by aliasing. Consider the following functions which are meant to serialize a 32-bit unsigned integer array into a byte array with little endian byte order.
std::array<std::byte, 4> serialize_le(unsigned x) {
return {
std::byte(x >> 0),
std::byte(x >> 8),
std::byte(x >> 16),
std::byte(x >> 24)
};
}
void write_le_n(std::byte* mem, std::array<unsigned, 1024>& numbers) {
for (unsigned n : numbers) {
auto bytes = serialize_le(n);
std::memcpy(mem, &bytes[0], sizeof(bytes));
mem += 4;
}
}
void write_le(std::byte mem[], unsigned x) {
mem[0] = std::byte(x >> 0);
mem[1] = std::byte(x >> 8);
mem[2] = std::byte(x >> 16);
mem[3] = std::byte(x >> 24);
}
void write_le_n(std::byte* mem, unsigned x[1024]) {
for (unsigned i = 0; i < 1024; ++i) {
write_le(mem + i * 4, x[i]);
}
}
See live example at Compiler Explorer
Intuitively, it would seem like both code samples are doing pretty much the same thing. However, the code generation for the second sample is much worse.
There is absolutely no auto-vectorization; it's an extremely naive loop.
std::array can often indicate to the compiler that no aliasing takes place, or if there is overlap between memory regions, it cannot be partial. This is a boost to optimizations.
Conclusion
std::array solves countless problems with C-style arrays.
For the most part, this isn't about performance, although std::array may be better in specific cases.
It's about C-style arrays having confusing syntax, common pitfalls, arbitrary restrictions, and other issues.
See Also
for (auto i = ++std::begin(myArray); . . .may not even compile (it seems that temporaries of fundamental type are not mutable, at least not with clang 6)struct Direction { int32_t dw; int32_t dh; };andstatic const Direction DIRECTIONS[DIRECTIONS_COUNT] { { -1, 1}, {0,1}, {1,1} , { 1, 0 }, {1,-1}, {0,-1} , {-1,-1}, {-1,0} };compiles. But if you change to astd::array<Direction,DIRECTIONS_COUNT>with same initializer list, suddenly you get "too many initializers" error. (VS 2019 Community with language = C++17)std::arrayis just a wrapper for C-style arrays. If I'm not mistaking, in some earlier versions of C++ you couldn't initialize it with single brackets. However, don't take my word for it, since I haven't used them much