Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Using Enumerations with std::array
Use enum values to create descriptive, self-documenting array indices.
std::array and enumerations
In the Array indexing and length using enumerators lesson, we discussed arrays and enumerations.
Now that we have constexpr std::array in our toolkit, we'll continue that discussion and show additional tricks.
Using static assert to ensure the proper number of array initializers
When initializing a constexpr std::array using CTAD, the compiler deduces how long the array should be from the number of initializers. If fewer initializers are provided than expected, the array will be shorter than expected, and indexing it can lead to undefined behavior.
For example:
#include <array>
#include <iostream>
enum Weekday
{
monday, // 0
tuesday, // 1
wednesday, // 2
thursday, // 3
friday, // 4
max_weekdays // 5
};
int main()
{
constexpr std::array hours { 8, 9, 7, 8 }; // oops, only 4 values
std::cout << "Friday hours: " << hours[Weekday::friday] << '\n'; // undefined behavior due to invalid index
return 0;
}
Whenever the number of initializers in a constexpr std::array can be sanity checked, you can do so using a static assert:
#include <array>
#include <iostream>
enum Weekday
{
monday, // 0
tuesday, // 1
wednesday, // 2
thursday, // 3
friday, // 4
max_weekdays // 5
};
int main()
{
constexpr std::array hours { 8, 9, 7, 8 };
// Ensure the number of hours matches the number of weekdays
static_assert(std::size(hours) == max_weekdays); // compile error: static_assert condition failed
std::cout << "Friday hours: " << hours[Weekday::friday] << '\n';
return 0;
}
That way, if you add a new enumerator later but forget to add a corresponding initializer to hours, the program will fail to compile.
You can also use a static assert to ensure two different constexpr std::array have the same length.
Using constexpr arrays for better enumeration input and output
Advanced note: Overloading I/O operators allows custom types to work with std::cin and std::cout. Helper functions can convert enumerators to strings and vice-versa.
Let's improve these functions. In cases where the value of our enumerators start at 0 and proceed sequentially (which is true for most enumerations), we can use an array to hold the name of each enumerator.
This allows us to do two things:
- Index the array using the enumerator's value to get the name of that enumerator.
- Use a loop to iterate through all of the names, and be able to correlate a name back to the enumerator based on index.
#include <array>
#include <iostream>
#include <string>
#include <string_view>
namespace Status
{
enum Type
{
pending,
approved,
denied,
max_statuses
};
// use sv suffix so std::array will infer type as std::string_view
using namespace std::string_view_literals; // for sv suffix
constexpr std::array statusName { "pending"sv, "approved"sv, "denied"sv };
// Make sure we've defined strings for all our statuses
static_assert(std::size(statusName) == max_statuses);
};
constexpr std::string_view getStatusName(Status::Type status)
{
// We can index the array using the enumerator to get the name of the enumerator
return Status::statusName[static_cast<std::size_t>(status)];
}
// Teach operator<< how to print a Status
std::ostream& operator<<(std::ostream& out, Status::Type status)
{
return out << getStatusName(status);
}
// Teach operator>> how to input a Status by name
std::istream& operator>> (std::istream& in, Status::Type& status)
{
std::string input {};
std::getline(in >> std::ws, input);
// Iterate through the list of names to see if we can find a matching name
for (std::size_t index=0; index < Status::statusName.size(); ++index)
{
if (input == Status::statusName[index])
{
// If we found a matching name, we can get the enumerator value based on its index
status = static_cast<Status::Type>(index);
return in;
}
}
// We didn't find a match, so input must have been invalid
// so we will set input stream to fail state
in.setstate(std::ios_base::failbit);
return in;
}
int main()
{
auto applicationStatus{ Status::approved };
std::cout << "Your application is " << applicationStatus << '\n';
std::cout << "Enter a new status: ";
std::cin >> applicationStatus;
if (!std::cin)
std::cout << "Invalid\n";
else
std::cout << "Your application is now " << applicationStatus << '\n';
return 0;
}
This prints:
Your application is approved Enter a new status: denied Your application is now denied
Range-based for-loops and enumerations
Occasionally we run across situations where it would be useful to iterate through the enumerators of an enumeration. While we can do this using a for-loop with an integer index, this is likely to require a lot of static casting of the integer index to our enumeration type.
#include <array>
#include <iostream>
#include <string_view>
namespace Status
{
enum Type
{
pending,
approved,
denied,
max_statuses
};
using namespace std::string_view_literals; // for sv suffix
constexpr std::array statusName { "pending"sv, "approved"sv, "denied"sv };
static_assert(std::size(statusName) == max_statuses);
};
constexpr std::string_view getStatusName(Status::Type status)
{
return Status::statusName[status];
}
std::ostream& operator<<(std::ostream& out, Status::Type status)
{
return out << getStatusName(status);
}
int main()
{
// Use a for loop to iterate through all our statuses
for (int i=0; i < Status::max_statuses; ++i )
std::cout << static_cast<Status::Type>(i) << '\n';
return 0;
}
Unfortunately, range-based for-loops won't allow you to iterate over the enumerators of an enumeration.
Since we can use a range-based for-loop on an array, one of the most straightforward solutions is to create a constexpr std::array containing each of our enumerators, and then iterate over that. This method only works if the enumerators have unique values.
#include <array>
#include <iostream>
#include <string_view>
namespace Status
{
enum Type
{
pending, // 0
approved, // 1
denied, // 2
max_statuses // 3
};
using namespace std::string_view_literals; // for sv suffix
constexpr std::array statusName { "pending"sv, "approved"sv, "denied"sv };
static_assert(std::size(statusName) == max_statuses);
constexpr std::array types { pending, approved, denied }; // A std::array containing all our enumerators
static_assert(std::size(types) == max_statuses);
};
constexpr std::string_view getStatusName(Status::Type status)
{
return Status::statusName[status];
}
std::ostream& operator<<(std::ostream& out, Status::Type status)
{
return out << getStatusName(status);
}
int main()
{
for (auto s: Status::types) // ok: we can do a range-based for on a std::array
std::cout << s << '\n';
return 0;
}
In the above example, since the element type of Status::types is Status::Type, variable s will be deduced as a Status::Type, which is exactly what we want!
This prints:
pending approved denied
Summary
Static assertions for array length: Use static_assert to verify that constexpr std::array initializers match the expected count enumerator. This catches mismatches at compile-time, preventing undefined behavior from accessing invalid indices.
Enum-to-string conversion: Create a constexpr std::array of std::string_view where indices correspond to enumerator values. Index the array using the enumerator to get its name. Works when enumerators start at 0 and proceed sequentially.
String-to-enum conversion: Loop through the name array, comparing input against each element. When a match is found, the index corresponds to the enumerator value. Cast the index back to the enumeration type.
Overloading I/O operators: Implement operator<< using the enum-to-string array for output, and operator>> using string-to-enum lookup for input. This allows enumerations to work naturally with std::cin and std::cout.
Range-based for loops with enums: Create a constexpr std::array containing all enumerators, then use a range-based for loop to iterate over it. Only works when enumerators have unique values. The loop variable is automatically deduced as the enum type.
sv suffix for string literals: Use "text"sv with using namespace std::string_view_literals; to ensure CTAD deduces std::string_view rather than const char* for array element types.
Coordinating multiple arrays: Use static assertions to ensure parallel arrays (like statusName and types) have the same length, preventing synchronization bugs.
The pattern of using constexpr std::array with enumerations creates elegant, type-safe code for enum I/O and iteration, with compile-time validation ensuring correctness.
Using Enumerations with std::array - Quiz
Test your understanding of the lesson.
Practice Exercises
Color Palette with Enum-to-String Conversion
Create a program that uses constexpr std::array with enumerations to implement enum-to-string conversion for a color palette. Practice using static_assert to ensure the array size matches the enum count.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!