c++enumsenumerationenum-class

What are commonly-used ways to iterate over an enum class in C++?


I am finding that all of my standard techniques for iterating over regular enums unfortunately do NOT work on enum classes since enum classes do not implicitly convert to integers.

NOT a duplicate of How can I iterate over an enum?, since I'm asking about an enum class (ie: a strongly-typed enum) and they are asking about a regular enum (ie: a weakly-typed enum).


Solution

  • Limitation for this entire answer: the enum has to be continuous, with no gaps (skipped integers not assigned to an enum member) between the two enum values you want to iterate between.

    Quick summary

    Option 0: use a C++20 range generator function to iterate over the enum class using a modern range-based for loop.

    In C++20 or later you can write an enum_range() range generator function to allow you to iterate over an enum or enum class (scoped enum) using modern range-based for loops like this:

    for (const MyErrorType enum_element : 
        enum_range(MyErrorType::begin, MyErrorType::end))
    {
        // switch statement here, operating on `enum_element`
    }
    

    Here is the custom enum_range() range generator function definition that works in C++20 or later. Comments in the function body have been removed for brevity:

    // Generate a C++20 "range" iterating from the enum value `first` to `last`,
    // inclusive, to be used in modern C++ range-based for loops.
    // - This is also using the C++20 feature of "abbreviated function templates",
    //   or "template argument deduction for functions", where `auto` can be used
    //   for all input parameter types and for the return type in place of making
    //   this a function template. 
    constexpr inline auto enum_range(auto first, auto last) 
    {
        auto enum_range = 
            std::views::iota(
                static_cast<std::underlying_type_t<decltype(first)>>(first), 
                static_cast<std::underlying_type_t<decltype(last)>>(last) + 1
            ) 
            | std::views::transform([](auto enum_val) 
                {
                    return (decltype(first))enum_val; 
                }
            ); 
    
        return enum_range; 
    };
    

    Explanation:

    enum_range() is a custom "abbreviated function template" function that generates a C++20 "range" object for iterating over enum values. It uses std::views::iota() (AKA: std::ranges::views::iota()) to create a sequence of underlying integer values from enum value MyErrorType::begin to MyErrorType::end, and then pipes that range to std::views::transform() with a lambda function as the operator argument to transform those integers back into the enum type. Piping the sequence to the std::views::transform() function is what generates the range object, which is then returned from the enum_range() function and used as the range container object in the range-based for loop for iterating through each enum element.

    Note: for those coming from C, don't confuse std::ranges::views::iota() with <cstdlib>'s itoa() function. In the former, iota refers to the Greek letter "iota", meaning: "a small increment or a sequence of consecutive values", and in the latter C function it stands for "integer to ASCII (C-string)".

    (End of the quick summary)

    See below for details and alternatives for earlier versions of C++.


    Details, and alternatives for C++11 through C++23, inclusive

    In C++20 or later, you can iterate over an enum or enum class (scoped enum) using range-based for loops by writing a custom range-generator function. While this range generator function is complicated to write, it is simple to use. And, 康桓瑋 (Kang Huanwei) paved the way in his answer here. That answer is where I first learned about ranges in C++.

    For C++11 or later, you can use a standard for loop with some casts. While this is less "modern", its advantages are that it is widely available in C++11 or later and is far more accessible and understandable to the average C++ programmer.

    Since enum classes (scoped enums) were not introduced until C++11, this answer does not work for C++03 or earlier at all.

    For all of my code below, see these files in my eRCaGuy_hello_world repo:

    1. enum_class_iterate.cpp
    2. enum_class_iterate_via_cpp20_range-based_views.cpp

    In C++23 or later

    Note that:

    The namespace alias std::views is provided as a shorthand for std::ranges::views.

    Defined in header <ranges>

    namespace std {
        namespace views = ranges::views;
    }
    

    Source: https://en.cppreference.com/w/cpp/ranges

    So, for this enum:

    enum class MyErrorType 
    {
        SOMETHING_1 = 0,
        SOMETHING_2,
        SOMETHING_3,
        SOMETHING_4,
        SOMETHING_5,
        // Helpers for iterating over the enum:
        // - Note: adding these helpers adds no new enum values, since `begin`
        //   already has the same value as `SOMETHING_1`, and `end` already has the
        //   same value as `SOMETHING_5`. These are just aliased names is all. 
        begin = 0,
        end = SOMETHING_5,
    };
    

    *Use this range generator function:

    // C++23 or later: using `std::to_underlying()`, a C++23 feature:
    
    #include <ranges>
    
    // Generate a C++20 "range" iterating from the enum value `first` to `last`,
    // inclusive, to be used in modern C++ range-based for loops.
    // - This is also using the C++20 feature of "abbreviated function templates",
    //   or "template argument deduction for functions", where `auto` can be used
    //   for all input parameter types and for the return type in place of making
    //   this a function template. 
    constexpr inline auto enum_range(auto first, auto last) 
    {
        // Note that "ranges" exist only in C++20 or later
        auto enum_range = 
            // `std::views::iota` is a C++20 range generator that auto-generates a
            // sequence of values from (param1) to (param2 - 1).
            // - See: https://en.cppreference.com/w/cpp/ranges/iota_view
            // - The word "iota" refers to the Greek letter "iota" which apparently
            //   is often used in math and computer science to represent a small
            //   increment or a sequence of consecutive values.
            // - In range factories, `|` is apparently the "pipe" operator, which is
            //   used to chain together range operations.
            std::views::iota(
                std::to_underlying(first), 
                std::to_underlying(last) + 1
            ) 
            | std::views::transform([](auto enum_val) 
                { 
                    return (decltype(first))enum_val; 
                }
            ); 
    
        return enum_range; 
    };
    

    ...to iterate over the enum class using a modern range-based for loop like this:

        for (const MyErrorType e : enum_range(MyErrorType::begin, MyErrorType::end))
        {
            switch (e)
            {
                case MyErrorType::SOMETHING_1:
                    printf("MyErrorType::SOMETHING_1\n");
                    break;
                case MyErrorType::SOMETHING_2:
                    printf("MyErrorType::SOMETHING_2\n");
                    break;
                case MyErrorType::SOMETHING_3:
                    printf("MyErrorType::SOMETHING_3\n");
                    break;
                case MyErrorType::SOMETHING_4:
                    printf("MyErrorType::SOMETHING_4\n");
                    break;
                case MyErrorType::SOMETHING_5:
                    printf("MyErrorType::SOMETHING_5\n");
                    break;
            }
        }
    

    In C++20 or later

    In C++20 and earlier you don't have access to std::to_underlying(my_variable), so you must use static_cast<std::underlying_type_t<decltype(my_variable)>>(my_variable) instead. So, here is the range generator function that works in C++20 or later:

    // For C++20 range-based views:
    
    // Generate a C++20 "range" iterating from the enum value `first` to `last`,
    // inclusive, to be used in modern C++ range-based for loops.
    // - This is also using the C++20 feature of "abbreviated function templates",
    //   or "template argument deduction for functions", where `auto` can be used
    //   for all input parameter types and for the return type in place of making
    //   this a function template. 
    constexpr inline auto enum_range(auto first, auto last) 
    {
        // Note that "ranges" exist only in C++20 or later
        auto enum_range = 
            // `std::views::iota` is a C++20 range generator that auto-generates a
            // sequence of values from (param1) to (param2 - 1).
            // - See: https://en.cppreference.com/w/cpp/ranges/iota_view
            // - The word "iota" refers to the Greek letter "iota" which apparently
            //   is often used in math and computer science to represent a small
            //   increment or a sequence of consecutive values.
            // - In range factories, `|` is apparently the "pipe" operator, which is
            //   used to chain together range operations.
            std::views::iota(
                static_cast<std::underlying_type_t<decltype(first)>>(first), 
                static_cast<std::underlying_type_t<decltype(last)>>(last) + 1
            ) 
            | std::views::transform([](auto enum_val) 
                {
                    return (decltype(first))enum_val; 
                }
            ); 
    
        return enum_range; 
    };
    

    Everything else is the same as above.

    In C++11 or later

    The "ranges" library (see: https://en.cppreference.com/w/cpp/ranges) was introduced in C++20, so you can't use it in C++11, C++14, or C++17. The following therefore works in C++11 or later, including in C++20 and C++23.

    While the above technique is very easy to use, the range generator is very hard to initially write, and to understand. Therefore, the following approach has the following benefits:

    1. It is the simplest for anyone to follow.
    2. It has the lowest barrier-to-entry.
    3. It works in any version of C++11 or later, rather than requiring C++20. And...
    4. It still has the possibility of introducing no new enum values into the enum. Rather, "option 1" of this approach below name-aliases a couple of the already-existing values in the enum for convenience.

    Additional info (also applies to the above examples): it compiles with the -Wall -Wextra -Werror compiler build options, which ensure that a compile-time error is thrown if you forget to handle any of the enum values in your switch statements. This is a great safety feature to ensure you keep the enum definition and all switch cases in-sync, handling all possible enums in all of your switch statements.

    Option 1: introduces no new enum values

    From enum_class_iterate.cpp in my eRCaGuy_hello_world repo:

    ///usr/bin/env ccache g++ -Wall -Wextra -Werror -O3 -std=gnu++17 "$0" -o /tmp/a && /tmp/a "$@"; exit
    // For the line just above, see my answer here: https://stackoverflow.com/a/75491834/4561887
    
    #include <cstdio>   // For `printf()`
    
    enum class MyErrorType 
    {
        SOMETHING_1 = 0,
        SOMETHING_2,
        SOMETHING_3,
        SOMETHING_4,
        SOMETHING_5,
        // Helpers for iterating over the enum:
        // - Note: adding these helpers adds no new enum values, since `begin`
        //   already has the same value as `SOMETHING_1`, and `end` already has the
        //   same value as `SOMETHING_5`. These are just aliased names is all. 
        begin = 0,
        end = SOMETHING_5,
    };
    
    int main()
    {
        printf("C++ enum class iteration demo.\n");
    
        // Iterate over the enum class
    
        // Option 1
        for (MyErrorType myErrorType = MyErrorType::begin;
            myErrorType <= MyErrorType::end;
            myErrorType = static_cast<MyErrorType>(
                static_cast<size_t>(myErrorType) + 1))
        {
            switch (myErrorType)
            {
                case MyErrorType::SOMETHING_1:
                    printf("MyErrorType::SOMETHING_1\n");
                    break;
                case MyErrorType::SOMETHING_2:
                    printf("MyErrorType::SOMETHING_2\n");
                    break;
                case MyErrorType::SOMETHING_3:
                    printf("MyErrorType::SOMETHING_3\n");
                    break;
                case MyErrorType::SOMETHING_4:
                    printf("MyErrorType::SOMETHING_4\n");
                    break;
                case MyErrorType::SOMETHING_5:
                    printf("MyErrorType::SOMETHING_5\n");
                    break;
            }
        }
    }
    

    Make the file above executable and run it as follows. Tested in Linux Ubuntu 22.04:

    # Ensure you have `ccache`
    sudo apt update
    sudo apt install ccache
    
    # make the file executable
    chmod +x enum_class_iterate.cpp
    
    # compile and run it
    ./enum_class_iterate.cpp
    

    Sample run and output:

    eRCaGuy_hello_world/cpp$ ./enum_class_iterate.cpp 
    C++ enum class iteration demo.
    MyErrorType::SOMETHING_1
    MyErrorType::SOMETHING_2
    MyErrorType::SOMETHING_3
    MyErrorType::SOMETHING_4
    MyErrorType::SOMETHING_5
    

    Option 2 (my preference): introduces a new count enum class value, but makes the last portion of the enum class definition always the same for all enum classes, which is nice

    This is my preferred way to define an enum class in C++, for all versions of C++, including C++11 through C++23, inclusive. For C++20, rather than the manual iteration approach below, however, I do like to use the enum_range() range generator function I showed above to iterate using modern range-based for loops.

    Note that you can also optionally add a count value if you need to use it anywhere to get the number of valid enum values in your enum class. This also has the really nice added benefit of making the last portion of the enum class definition, starting with count, always the exact same for all enum classes, so you can easily copy-paste this code and recognize it throughout your code base. In the first example above, in end = SOMETHING_5,, the SOMETHING_5 enum value must be manually updated for all of your enums, which is error-prone. So, this is my preferred way to define an enum class in C++:

    enum class MyErrorType2 
    {
        SOMETHING_1 = 0,
        SOMETHING_2,
        SOMETHING_3,
        SOMETHING_4,
        SOMETHING_5,
    
        // Helpers
        count,
        begin = 0,
        end = count - 1,
    };
    

    Iteration over the enum is still exactly the same as above, except you must add a dummy "nothing to do" case for the count value in your switch statement, in order to cover all possible values in the enum class:

    // Option 2: same as above, except we must also include the `count` value
    // as a switch case. 
    for (MyErrorType2 myErrorType2 = MyErrorType2::begin;
        myErrorType2 <= MyErrorType2::end;
        myErrorType2 = static_cast<MyErrorType2>(
            static_cast<size_t>(myErrorType2) + 1))
    {
        switch (myErrorType2)
        {
            case MyErrorType2::SOMETHING_1:
                printf("MyErrorType2::SOMETHING_1\n");
                break;
            case MyErrorType2::SOMETHING_2:
                printf("MyErrorType2::SOMETHING_2\n");
                break;
            case MyErrorType2::SOMETHING_3:
                printf("MyErrorType2::SOMETHING_3\n");
                break;
            case MyErrorType2::SOMETHING_4:
                printf("MyErrorType2::SOMETHING_4\n");
                break;
            case MyErrorType2::SOMETHING_5:
                printf("MyErrorType2::SOMETHING_5\n");
                break;
            case MyErrorType2::count:
                // Nothing to do
                break;
        }
    }
    

    If you forget the case MyErrorType2::count: case in the switch statement but are compiling using the compiler's -Wall -Wextra -Werror options, you will get this error: error: enumeration value ‘count’ not handled in switch [-Werror=switch], as shown here:

    eRCaGuy_hello_world/cpp$ ./enum_class_iterate.cpp 
    ./enum_class_iterate.cpp: In function ‘int main()’:
    ./enum_class_iterate.cpp:127:16: error: enumeration value ‘count’ not handled in switch [-Werror=switch]
      127 |         switch (myErrorType2)
          |                ^
    cc1plus: all warnings being treated as errors
    

    Compiling with -Wall -Wextra -Werror and not using a default switch case in any switch statements is therefore the recommended approach to help you always keep your enum definitions and switch statements in-sync, covering all possible enum values.

    References

    1. *****+ This really helpful answer by 康桓瑋 (Kang Huanwei) here

    2. C++20 views and ranges: https://en.cppreference.com/w/cpp/ranges

      1. https://en.cppreference.com/w/cpp/ranges/iota_view
    3. Algorithm: https://en.cppreference.com/w/cpp/algorithm/transform

    4. Utility: https://en.cppreference.com/w/cpp/utility/to_underlying

    5. https://en.cppreference.com/w/cpp/language/decltype

    6. GitHub Copilot:

      Prompt:

      In std::views::iota, what does iota stand for?

      Answer (emphasis added):

      In std::views::iota, the term "iota" is derived from the Greek letter "ι" (iota), which is often used in mathematics and computer science to represent a small increment or a sequence of consecutive values. In the context of C++20 ranges, std::views::iota generates an infinite sequence of values starting from a given initial value.

    7. Abbreviated Function Templates and Constrained Auto - https://devblogs.microsoft.com/cppblog/abbreviated-function-templates-and-constrained-auto/

    Related

    1. Common techniques for iterating over enums (as opposed to enum classes): How can I iterate over an enum?
      1. [my answer] How can I iterate over an enum?
    2. My answer on some of the differences between enum classes (strongly-typed enums) and regular enums (weakly-typed enums) in C++: How to automatically convert strongly typed enum into int?
    3. Some of my personal notes on the -Wall -Wextra -Werror and other build options, from my eRCaGuy_hello_world repo.
    4. Incrementation and decrementation of “enum class”

    Other keywords: common way to iterate over enum or enum class in C or C++; best way to iterate over enum class in C++; enum class C++ iterate; c++ iterate over enum class