Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Working with std::array of Custom Types
Initialize arrays of objects with nested braces and understand elision rules.
std::array of class types and brace elision
A std::array can contain elements of any object type, including compound types. This means you can create a std::array of pointers, or a std::array of structs (or classes).
However, initializing a std::array of structs or classes can be confusing for new programmers, so we'll spend this lesson explicitly covering this topic.
Advanced note: The examples in this lesson use structs, but the concepts apply equally to classes.
Defining and assigning to a std::array of structs
Let's start with a simple struct:
struct Item
{
int id{};
double weight{};
bool isStackable{};
};
Defining a std::array of Item and assigning elements works as expected:
#include <array>
#include <iostream>
struct Item
{
int id{};
double weight{};
bool isStackable{};
};
int main()
{
std::array<Item, 3> inventory{};
inventory[0] = { 101, 2.5, true };
inventory[1] = { 102, 8.0, false };
inventory[2] = { 103, 0.5, true };
for (const auto& item : inventory)
{
std::cout << "Item " << item.id
<< " weighs " << item.weight << '\n';
}
return 0;
}
The above outputs:
Item 101 weighs 2.5 Item 102 weighs 8 Item 103 weighs 0.5
Initializing a std::array of structs
Initializing an array of structs works as expected, as long as you explicitly specify the element type:
#include <array>
#include <iostream>
struct Item
{
int id{};
double weight{};
bool isStackable{};
};
int main()
{
constexpr std::array inventory{ // use CTAD to deduce template arguments <Item, 3>
Item{ 101, 2.5, true },
Item{ 102, 8.0, false },
Item{ 103, 0.5, true }
};
for (const auto& item : inventory)
{
std::cout << "Item " << item.id
<< " weighs " << item.weight << '\n';
}
return 0;
}
In this example, we're using CTAD to deduce the type of the std::array as std::array<Item, 3>. We then provide 3 Item objects as initializers, which works perfectly.
Initialization without explicitly specifying the element type for each initializer
In the example above, you'll notice that each initializer requires us to specify the element type:
constexpr std::array inventory{
Item{ 101, 2.5, true }, // we mention Item here
Item{ 102, 8.0, false }, // and here
Item{ 103, 0.5, true } // and here
};
But we didn't have to do the same in the assignment case:
// The compiler knows that each element of inventory is an Item
// so it will implicitly convert the right hand side of each assignment to an Item
inventory[0] = { 101, 2.5, true };
inventory[1] = { 102, 8.0, false };
inventory[2] = { 103, 0.5, true };
So you might attempt something like this:
// doesn't work
constexpr std::array<Item, 3> inventory{ // we're telling the compiler that each element is an Item
{ 101, 2.5, true }, // but not mentioning it here
{ 102, 8.0, false },
{ 103, 0.5, true }
};
Surprisingly, this doesn't work. Let's explore why.
A std::array is defined as a struct containing a single C-style array member (whose name is implementation defined), like this:
template<typename T, std::size_t N>
struct array
{
T implementation_defined_name[N]; // a C-style array with N elements of type T
}
Advanced note: C-style arrays are covered in a later lesson. For now, just know that T implementation_defined_name[N]; is a fixed-size array of N elements of type T.
When we try to initialize inventory as shown above, the compiler interprets the initialization like this:
// Doesn't work
constexpr std::array<Item, 3> inventory{ // initializer for inventory
{ 101, 2.5, true }, // initializer for C-style array member with implementation_defined_name
{ 102, 8.0, false }, // ?
{ 103, 0.5, true } // ?
};
The compiler interprets { 101, 2.5, true } as the initializer for the first member of inventory, which is the C-style array with the implementation defined name. This initializes C-style array element 0 with { 101, 2.5, true } and the remaining members are zero-initialized. Then the compiler discovers we've provided two more initialization values ({ 102, 8.0, false } and { 103, 0.5, true }) and produces a compilation error telling us we've provided too many values.
The correct way to initialize the above is to add an extra set of braces:
// This works as expected
constexpr std::array<Item, 3> inventory{ // initializer for inventory
{ // extra set of braces to initialize the C-style array member with implementation_defined_name
{ 101, 2.5, true }, // initializer for array element 0
{ 102, 8.0, false }, // initializer for array element 1
{ 103, 0.5, true }, // initializer for array element 2
}
};
Note the extra set of braces required (to begin initialization of the C-style array member inside the std::array struct). Within those braces, we can then initialize each element individually, each inside its own set of braces.
This is why you'll see std::array initializers with an extra set of braces when the element type requires a list of values and we are not explicitly providing the element type as part of the initializer.
When initializing a `std::array` with a struct, class, or array and not providing the element type with each initializer, you'll need an extra pair of braces so the compiler will properly interpret what to initialize.
This is an artifact of aggregate initialization, and other standard library container types (that use list constructors) do not require double braces in these cases.
Here's a full example:
#include <array>
#include <iostream>
struct Item
{
int id{};
double weight{};
bool isStackable{};
};
int main()
{
constexpr std::array<Item, 3> inventory{{ // note double braces
{ 101, 2.5, true },
{ 102, 8.0, false },
{ 103, 0.5, true }
}};
for (const auto& item : inventory)
{
std::cout << "Item " << item.id
<< " weighs " << item.weight << '\n';
}
return 0;
}
Brace elision for aggregates
Given the explanation above, you may wonder why the above case requires double braces, but all other cases we've seen only require single braces:
#include <array>
#include <iostream>
int main()
{
constexpr std::array<int, 5> scores{ 85, 92, 78, 90, 88 }; // single braces
for (const auto score : scores)
std::cout << score << '\n';
return 0;
}
It turns out that you can supply double braces for such arrays:
#include <array>
#include <iostream>
int main()
{
constexpr std::array<int, 5> scores{{ 85, 92, 78, 90, 88 }}; // double braces
for (const auto score : scores)
std::cout << score << '\n';
return 0;
}
However, aggregates in C++ support a concept called brace elision, which lays out rules for when multiple braces may be omitted. Generally, you can omit braces when initializing a std::array with scalar (single) values, or when initializing with class types or arrays where the type is explicitly named with each element.
There is no harm in always initializing std::array with double braces, as it avoids having to think about whether brace-elision applies in a specific case. Alternatively, you can try single-brace initialization, and the compiler will generally complain if it can't figure it out. In that case, you can quickly add an extra set of braces.
Another example
Here's one more example where we initialize a std::array with Enemy structs.
#include <array>
#include <iostream>
#include <string_view>
// Each enemy has an ID and a name
struct Enemy
{
int id{};
std::string_view name{};
};
// Our array of 3 enemies (single braced since we mention Enemy with each initializer)
constexpr std::array enemies{ Enemy{1, "Goblin"}, Enemy{2, "Skeleton"}, Enemy{3, "Orc"} };
const Enemy* findEnemyById(int id)
{
// Look through all the enemies
for (auto& enemy : enemies)
{
// Return enemy with matching ID
if (enemy.id == id) return &enemy;
}
// No matching ID found
return nullptr;
}
int main()
{
constexpr std::string_view notFound{ "not found" };
const Enemy* e1{ findEnemyById(2) };
std::cout << "You found: " << (e1 ? e1->name : notFound) << '\n';
const Enemy* e2{ findEnemyById(5) };
std::cout << "You found: " << (e2 ? e2->name : notFound) << '\n';
return 0;
}
This prints:
You found: Skeleton You found: not found
Note that because std::array enemies is constexpr, our findEnemyById() function must return a const pointer, which means our Enemy pointers in main() must also be const.
Summary
Arrays of compound types: std::array can contain any object type, including pointers, structs, and classes. Initialization syntax varies depending on whether you explicitly specify the element type with each initializer.
Assignment works as expected: Assigning to individual array elements or using aggregate assignment with structs works straightforwardly, just like with simple types.
CTAD with explicit types: When using CTAD and explicitly mentioning the element type with each initializer (like Item{ 101, 2.5, true }), single braces work fine. The compiler deduces std::array<Item, 3> from three Item initializers.
Double braces requirement: When initializing without explicitly specifying the element type for each initializer, you need double braces. The outer braces initialize the std::array, and the inner braces initialize the C-style array member inside std::array.
Why double braces: std::array is implemented as a struct containing a C-style array member. Without explicit element types, the compiler interprets the first set of braces as initializing that C-style array member, requiring a second set to properly initialize all elements.
Brace elision: C++ allows omitting braces when initializing aggregates with scalar values or when explicitly naming types. You can always use double braces to avoid thinking about elision rules, or try single braces and let the compiler complain if needed.
Constexpr arrays of classes: Constexpr std::array of class types works perfectly for compile-time lookup tables and search functions, as demonstrated by the enemy lookup example.
The key takeaway is that std::array's aggregate nature sometimes requires extra brace pairs, but this is a minor syntax inconvenience for a powerful compile-time container.
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!