Working with std::array of Custom Types
Initialize arrays of objects with nested braces and understand elision rules.
What Is a std::array of Class Types?
Nothing about std::array restricts its element type to int and double. An element can be any object type, which includes pointers, other arrays, and the structs and classes you define yourself. A fleet of kayaks is as valid an element type as a depth reading:
struct Kayak
{
int hullId {};
double lengthMetres {};
int seatCount {};
};
The container works exactly as you would expect once it exists. What trips people up is getting it into existence, because a std::array<Kayak, 3> sometimes needs one pair of braces and sometimes needs two, and the reason is not visible from the outside. This lesson works from the reason outwards, so the brace count stops being something to memorise.
The examples all use a struct, but every point applies unchanged to classes.
The One Fact That Explains Every Brace
std::array is not a magic language construct. It is an ordinary struct with one member, and that member is a C-style array:
template <typename T, std::size_t N>
struct array
{
T elements[N]; // one member: a fixed-size array of N objects of type T
};
The real thing has member functions and a reserved name for that member, but this is the shape that matters. On the library shipped with this platform the member is spelled _M_elems; the standard deliberately leaves the name unspecified, so never write it in your own code.
That single member is the whole explanation. When you write braces after a std::array object, the outermost pair belongs to the std::array struct itself, not to the elements. Anything you want to hand to the elements lives one level deeper, inside a second pair that initializes the C-style array member.
The outer braces initialize the
std::array struct. The inner braces initialize its one member, the C-style array. Element values sit inside that second pair.
Watching Single Braces Fail
With that picture in mind, the classic mistake stops being mysterious. The following program is broken on purpose. It names the element type on the array (std::array<Kayak, 3>) and then supplies three braced element values directly:
#include <array>
#include <iostream>
struct Kayak
{
int hullId {};
double lengthMetres {};
int seatCount {};
};
int main()
{
constexpr std::array<Kayak, 3> fleet {
{ 41, 4.2, 1 },
{ 58, 5.6, 2 },
{ 73, 3.9, 1 }
};
for (const auto& kayak : fleet)
{
std::cout << "Hull " << kayak.hullId << ": " << kayak.lengthMetres
<< " m, " << kayak.seatCount << " seat(s)\n";
}
return 0;
}
s.cpp: In function 'int main()':
s.cpp:17:5: error: too many initializers for 'const std::array<Kayak, 3>'
17 | };
| ^
"Too many initializers" for an array of exactly three elements given exactly three values looks absurd until you count members instead of elements. The std::array struct has one member. { 41, 4.2, 1 } was taken as the initializer for that member, filling the C-style array. The remaining two braced groups had no member left to initialize, so the compiler reported a surplus.
Restoring the Missing Brace Pair
The fix is to write the level that was skipped:
#include <array>
#include <iostream>
struct Kayak
{
int hullId {};
double lengthMetres {};
int seatCount {};
};
int main()
{
constexpr std::array<Kayak, 3> fleet {{ // note the second pair of braces
{ 41, 4.2, 1 },
{ 58, 5.6, 2 },
{ 73, 3.9, 1 }
}};
for (const auto& kayak : fleet)
{
std::cout << "Hull " << kayak.hullId << ": " << kayak.lengthMetres
<< " m, " << kayak.seatCount << " seat(s)\n";
}
return 0;
}
Hull 41: 4.2 m, 1 seat(s)
Hull 58: 5.6 m, 2 seat(s)
Hull 73: 3.9 m, 1 seat(s)
Now each brace pair has a job. The outer pair initializes fleet, the second pair initializes its C-style array member, and each innermost pair initializes one Kayak. This is why you see {{ and }} around std::array initializers so often once the element type takes more than one value.
This double-brace requirement is a consequence of std::array being an aggregate. Containers such as std::vector are initialized through a list constructor instead, so they never need the extra pair.
Naming the Element Type Instead
There is a second way out, and it does not involve counting braces at all. If you write the element type in front of each set of values, each initializer becomes a complete Kayak object rather than a loose list, and one pair of braces is enough:
#include <array>
#include <iostream>
struct Kayak
{
int hullId {};
double lengthMetres {};
int seatCount {};
};
int main()
{
constexpr std::array fleet { // deduced as std::array<Kayak, 3>
Kayak { 41, 4.2, 1 },
Kayak { 58, 5.6, 2 },
Kayak { 73, 3.9, 1 }
};
for (const auto& kayak : fleet)
{
std::cout << "Hull " << kayak.hullId << ": " << kayak.lengthMetres
<< " m, " << kayak.seatCount << " seat(s)\n";
}
return 0;
}
Hull 41: 4.2 m, 1 seat(s)
Hull 58: 5.6 m, 2 seat(s)
Hull 73: 3.9 m, 1 seat(s)
Naming the type also unlocks CTAD. Because the compiler can see three Kayak objects, it deduces std::array<Kayak, 3> on its own and the template arguments can be dropped from the declaration. The two benefits arrive together, which is why this form is common in real code.
Where Brace Elision Applies
By now a fair question is why std::array<int, 4> depthsMetres { 7, 12, 4, 19 }; has worked all along, since it also skips the level that belongs to the C-style array member.
The answer is a rule called brace elision, which lets aggregate initialization omit inner braces when the compiler can unambiguously work out which sub-object each value belongs to. Scalar values are unambiguous, so the inner pair may be dropped. You are free to write it out anyway:
#include <array>
#include <iostream>
int main()
{
constexpr std::array<int, 4> depthsMetres { 7, 12, 4, 19 }; // inner braces elided
constexpr std::array<int, 4> heightsMetres {{ 3, 8, 15, 6 }}; // inner braces written out
for (const auto depth : depthsMetres)
std::cout << depth << ' ';
std::cout << '\n';
for (const auto height : heightsMetres)
std::cout << height << ' ';
std::cout << '\n';
return 0;
}
7 12 4 19
3 8 15 6
Both forms compile and both mean the same thing. Elision also applies when each initializer names its element type, which is why the CTAD version above needed only single braces. It does not apply when the elements are class types and the values arrive as bare lists, which is exactly the case that failed earlier.
Collecting all of that into one place:
| What you write for each element | Braces needed | Why |
|---|---|---|
Scalar values (7, 12, 4, 19) |
one pair is enough | elision resolves the ambiguity |
The element type named (Kayak { 41, 4.2, 1 }) |
one pair is enough | each initializer is already a complete object |
Bare lists of member values ({ 41, 4.2, 1 }) |
two pairs required | elision does not apply, so the array member needs its own pair |
| Anything at all | two pairs always work | the extra pair is never wrong |
Pick one habit and keep it. Either always write the double braces, which is correct in every case, or name the element type with each initializer, which is correct in every case and gives you CTAD as well. The compiler catches the remaining case, so a build error simply means adding one more pair.
Assigning Elements After the Fact
Everything above concerns initialization. Assignment never has this problem, because the left side already tells the compiler exactly what type is expected:
#include <array>
#include <iostream>
struct Kayak
{
int hullId {};
double lengthMetres {};
int seatCount {};
};
int main()
{
std::array<Kayak, 3> fleet {};
fleet[0] = { 41, 4.2, 1 };
fleet[1] = { 58, 5.6, 2 };
fleet[2] = { 73, 3.9, 1 };
for (const auto& kayak : fleet)
{
std::cout << "Hull " << kayak.hullId << " seats " << kayak.seatCount << '\n';
}
return 0;
}
Hull 41 seats 1
Hull 58 seats 2
Hull 73 seats 1
fleet[0] is a Kayak, so { 41, 4.2, 1 } can only be a Kayak. There is no array member in sight and nothing to disambiguate. It is worth seeing this contrast deliberately, because "it worked when I assigned it" is the usual reason people expect single braces to work when initializing.
A Compile-Time Lookup Table
Putting the pieces together, a constexpr std::array of structs makes an excellent fixed lookup table. Here a set of radio beacons is searched by channel:
#include <array>
#include <iostream>
#include <string_view>
struct Beacon
{
int channel {};
std::string_view callSign {};
};
constexpr std::array beacons { Beacon { 12, "Kestrel" }, Beacon { 19, "Marlin" }, Beacon { 27, "Puffin" } };
const Beacon* findBeaconByChannel(int channel)
{
for (const auto& beacon : beacons)
{
if (beacon.channel == channel)
return &beacon;
}
return nullptr;
}
int main()
{
constexpr std::string_view unlisted { "no beacon on that channel" };
const Beacon* tuned { findBeaconByChannel(19) };
std::cout << "Channel 19: " << (tuned ? tuned->callSign : unlisted) << '\n';
const Beacon* missing { findBeaconByChannel(40) };
std::cout << "Channel 40: " << (missing ? missing->callSign : unlisted) << '\n';
return 0;
}
Channel 19: Marlin
Channel 40: no beacon on that channel
The initializer names Beacon with each element, so single braces are correct and CTAD deduces std::array<Beacon, 3>.
Because
beacons is constexpr, its elements are const. A pointer into it must therefore be const Beacon*, both as the return type of findBeaconByChannel() and at every call site. Dropping either const is a compile error, not a stylistic choice.
Returning nullptr for a channel with no beacon is what forces the conditional at each call site. That pattern generalises well: a compile-time table plus a search function that reports failure by pointer costs nothing at runtime for the table itself.
Key Terminology
Aggregate - a class or array type with no user-declared constructors, no private or protected non-static data members, and no virtual functions. Aggregates are initialized member by member from a braced list, and std::array is one.
Brace elision - the rule allowing inner braces to be omitted during aggregate initialization when each value's destination is unambiguous.
CTAD (class template argument deduction) - deducing a class template's arguments from its initializers, letting std::array fleet { Kayak { 41, 4.2, 1 }, ... } stand in for std::array<Kayak, 3>.
List constructor - a constructor taking a std::initializer_list, used by containers such as std::vector. Types with one are not aggregates and never need the extra brace pair.
Looking Forward
The C-style array hiding inside std::array is a real language feature, and the next lessons cover it directly: how it is declared, how it decays to a pointer when passed to a function, and why std::array exists to wrap it. Once you have seen C-style arrays on their own, the double-brace rule reads as an obvious consequence of the wrapper rather than as a quirk of the container.
Summary
Any object type works as an element - std::array holds structs, classes, and pointers as readily as fundamental types. Only the initialization syntax needs care.
std::array is a struct with one member - that member is a C-style array. The outer brace pair initializes the struct, the inner pair initializes the array member, and element values sit inside the inner pair.
Bare lists of member values need two brace pairs - std::array<Kayak, 3> fleet {{ { 41, 4.2, 1 }, ... }};. Writing one pair makes the compiler treat the first group as the whole array member and report "too many initializers".
Naming the element type needs only one - Kayak { 41, 4.2, 1 } is already a complete object, so no ambiguity remains, and CTAD can then deduce std::array<Kayak, 3> from the initializers.
Brace elision is the general rule - inner braces may be omitted whenever each value's destination is unambiguous, which covers scalar values and explicitly typed elements but not bare member lists for class types.
Assignment is never affected - fleet[0] = { 41, 4.2, 1 }; works because the left side already fixes the type.
A constexpr array of structs is a free lookup table - and because its elements are const, any pointer into it must be a pointer to const.
Two brace pairs are always safe and one is often enough. Knowing that std::array is a struct wrapping an array tells you which case you are in without guessing.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Working with std::array of Custom Types - Quiz
Test your understanding of the lesson.
Practice Exercises
Employee Record Array with Double Braces
Create a program that stores employee records in a std::array of structs. Practice proper initialization using double braces when not explicitly specifying the struct type for each element.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!