Array indexing and length using enumerators

Integer indices lack semantic meaning, making code harder to understand and maintain. Consider a game inventory system:

#include <vector>

int main()
{
    std::vector inventory { 15, 8, 42 };

    inventory[1] = 12;  // What does index 1 represent?

    return 0;
}

What item is at index 1? Without context, this code is cryptic.

Using enumerators for self-documenting indices

Unscoped enumerations implicitly convert to integral types, making them perfect for array indices:

#include <vector>

namespace Items
{
    enum Type
    {
        health_potions,
        mana_potions,
        arrows,
        max_items
    };
}

int main()
{
    std::vector inventory { 15, 8, 42 };

    inventory[Items::mana_potions] = 12;  // Much clearer!

    return 0;
}

Now it's obvious: we're updating the mana potion count. Because enumerators are implicitly constexpr, their conversion to std::size_t isn't considered narrowing.

Using non-constexpr enumerator variables

Enumerators themselves are constexpr, but variables of enumeration type aren't necessarily:

#include <vector>

namespace Items
{
    enum Type
    {
        health_potions,
        mana_potions,
        arrows,
        max_items
    };
}

int main()
{
    std::vector inventory { 15, 8, 42 };
    Items::Type item { Items::mana_potions };  // Non-constexpr

    inventory[item] = 12;  // May trigger sign conversion warning

    return 0;
}

If the enumeration's underlying type is signed (implementation-defined), this might generate warnings. Two solutions:

Option 1: Make the variable constexpr (when possible):

constexpr Items::Type item { Items::mana_potions };
inventory[item] = 12;  // No warning

Option 2: Specify an unsigned underlying type:

namespace Items
{
    enum Type : unsigned int  // Explicitly unsigned
    {
        health_potions,
        mana_potions,
        arrows,
        max_items
    };
}

Using a count enumerator

Add a final enumerator representing the count of preceding enumerators:

#include <iostream>
#include <vector>

namespace Items
{
    enum Type
    {
        health_potions,
        mana_potions,
        arrows,
        // Add new items here
        max_items  // Automatically equals the count
    };
}

int main()
{
    std::vector<int> inventory(Items::max_items);  // Creates 3-element vector

    inventory[Items::arrows] = 42;

    std::cout << "Tracking " << Items::max_items << " item types\n";

    return 0;
}

The count enumerator automatically updates when you add items. If we add gems before max_items:

namespace Items
{
    enum Type
    {
        health_potions,
        mana_potions,
        arrows,
        gems,          // New item
        max_items      // Now equals 4
    };
}

int main()
{
    std::vector<int> inventory(Items::max_items);  // Now creates 4-element vector
    // Everything still works!

    return 0;
}

Asserting on array length

When initializing an array with specific values, assert that the size matches your count enumerator:

#include <cassert>
#include <iostream>
#include <vector>

enum Planets
{
    mercury,
    venus,
    earth,
    mars,
    max_planets
};

int main()
{
    std::vector distanceFromSun { 58, 108, 150, 228 };  // Millions of km

    assert(std::size(distanceFromSun) == max_planets);

    std::cout << "Earth is " << distanceFromSun[earth] << " million km from the sun\n";

    return 0;
}

If the number of initializers doesn't match max_planets, the assert catches the error immediately.

Best Practice
Use `static_assert` for constexpr arrays, `assert` for non-constexpr arrays.

Enum classes and array indices

Enum classes don't implicitly convert to integers, requiring explicit casts:

#include <iostream>
#include <vector>

enum class Planets
{
    mercury,
    venus,
    earth,
    mars,
    max_planets
};

int main()
{
    std::vector distanceFromSun { 58, 108, 150, 228 };

    // Error: no implicit conversion
    std::cout << distanceFromSun[Planets::earth] << '\n';

    // OK: explicit cast
    std::cout << distanceFromSun[static_cast<int>(Planets::earth)] << '\n';

    return 0;
}

While verbose, a helper function improves readability:

#include <iostream>
#include <type_traits>
#include <vector>

enum class Planets
{
    mercury,
    venus,
    earth,
    mars,
    max_planets
};

// Overload unary + to convert enum class to underlying type
constexpr auto operator+(Planets p) noexcept
{
    return static_cast<std::underlying_type_t<Planets>>(p);
}

int main()
{
    std::vector distanceFromSun { 58, 108, 150, 228 };

    std::cout << distanceFromSun[+Planets::earth] << '\n';  // Cleaner!

    return 0;
}

For heavy enum class usage, consider using a regular enum in a namespace instead.

Summary

Enumerators as array indices: Unscoped enumerations implicitly convert to integral types, making them ideal for self-documenting array indices. Using named enumerators like Items::mana_potions instead of magic numbers like 1 makes code much more readable and maintainable.

Count enumerators: Adding a final enumerator (like max_items) that represents the count of preceding enumerators allows automatic tracking of array length. When you add new enumerators, the count updates automatically, making arrays sized with this value adjust accordingly.

Asserting array length: Use assert() or static_assert() to verify that initialized arrays have the expected number of elements. This catches mismatches between initializer counts and enum counts immediately.

Non-constexpr enum variables: Variables of enumeration type (unlike enumerators themselves) may require special handling. Make them constexpr when possible, or specify an unsigned underlying type for the enum to avoid sign conversion warnings.

Enum classes require casting: Enum classes don't implicitly convert to integers, requiring explicit static_cast or helper functions (like overloading unary +) for array indexing. For frequent array indexing, regular enums in namespaces may be more practical.

Using enumerators with arrays combines type safety with readability, creating self-documenting code that's easier to maintain as requirements evolve.