7

I want to use a C++ enum class as std:array index without calling an explicit cast, when I want to refer to a specific index.

I furthermore use a typedef for a fixed sized std::array.

typedef std::array<int, 3> MyType;
enum class MyEnum {
  ENUMERATOR0 = 0,
  ENUMERATOR1 = 1,
  ENUMERATOR2 = 2,
};

So instead of using:

MyType my_type = {0};
my_type[static_cast<int>(MyEnum::ENUMERATOR0)] = 42;

I want to use:

my_type[MyEnum::ENUMERATOR0] = 42;

Therefore, I assume it is required to overload the subscript operator of my MyType (std::array) type. However, I couldn't figure out, how to overload the subscript operator in my case. For simplicity reasons, I would like to avoid using a class instead of the typedef. How can I do this?

9
  • I just use a macro: #define INDEX(a) static_cast<int>(a) Commented Dec 25, 2017 at 22:45
  • If you don't mind using enum MyEnum, i.e. without the class in there, there should be no problem. Commented Dec 25, 2017 at 22:46
  • Is there a reason for using enum class? What is the use-case? What problem is it supposed to solve? Commented Dec 25, 2017 at 22:47
  • 2
    @Someprogrammerdude the use of the enum is supposed to enhance the codes readability. I furthermore don't want to use the C style enum, in order to have different scopes. Commented Dec 25, 2017 at 22:59
  • 2
    enum class and therefore different scopes in order to be able to reuse an enumerator in two different enums. I must admit, that that isn't a problem so far, but I don't want to get in trouble at a later stage. Commented Dec 26, 2017 at 10:00

5 Answers 5

6

I found a good solution for this. You can both use enum classes as indices in your arrays and also get the added benefit of ensuring type safety (i.e. preventing use of the wrong enum type as indices) by subclassing std::array and overriding its operator[] method.

Here is an example.

You can define enum_array like this:

#include <array>

// this is a new kind of array which accepts and requires its indices to be enums
template<typename E, class T, std::size_t N>
class enum_array : public std::array<T, N> {
public:
    T & operator[] (E e) {
        return std::array<T, N>::operator[]((std::size_t)e);
    }

    const T & operator[] (E e) const {
        return std::array<T, N>::operator[]((std::size_t)e);
    }
};

And you can use it like this:

int main() {
    enum class Fruit : unsigned int {
        Apple,
        Kiwi
    };

    enum class Vegetable : unsigned int {
        Carrot,
        Potato
    };

    // Old way:
    std::array<int, 3> old_fruits;
    std::array<int, 3> old_veggies;

    old_fruits[(int)Fruit::Apple] = 3;          // compiles but "ugly"
    old_veggies[(int)Vegetable::Potato] = 7;    // compiles but "ugly"

    old_fruits[(int)Vegetable::Potato] = 3;     // compiles but shouldn't compile!
    old_fruits[2] = 6;                          // compiles but may or may not be desirable

    // New way:
    enum_array<Fruit, int, 3> fruits;
    enum_array<Vegetable, int, 3> veggies;

    fruits[Fruit::Apple] = 3;
    veggies[Vegetable::Potato] = 7;

    // fruits[Vegetable::Potato] = 3;   // doesn't compile :)
    // fruits[2] = 6;                   // doesn't compile
    // fruits[(int)Fruit::Apple] = 3;   // doesn't compile
} 
Sign up to request clarification or add additional context in comments.

1 Comment

Standard containers are generally not designed to be derived from (ie, no virtual destructor, etc). Encapsulation is generally a better choice than Inheritance in this case.
1

Not enough reputation, so I can't comment... On jordi's solution, replace old style casts:

(std::size_t)e

with new style:

static_cast<std::size_t>(e)

This will avoid compile warnings...

Comments

1

You are not allowed to override [] on a type you do not own.

See http://en.cppreference.com/w/cpp/language/operators -- operaror[] cannot be non-member overloaded.

You can do this:

template<class E, class T, std::size_t N=E::count>
struct enum_array: std::array<T, N>{
  using base= std::array<T, N>;
  constexpr enum_array():base{}{}
  template<class A0, class...Args,
    std::enable_if_t<!std::is_same<T, std::decay_t<A0>>{}, bool>=true
  >
  constexpr enum_array(A0&& a0, Args&&...args):base{{std::forward<A0>(a0), std::forward<Args>(args)...}}{}
  // using base::operator[]; // -- if you want to expose [size_t] as well
  constexpr T& operator[](E const& e){ return base::operator[](static_cast<std::size_t>(e)); }
  constexpr T const& operator[](E const& e)const{ return base::operator[](static_cast<std::size_t>(e)); }
};

which is close. Replace

MyType x={{1,2,3}};

with

enum_array<MyEnum, int> x={1,2,3};

and add count to MyEnum.

Comments

1

You can emulate an enum with a class:

struct MyEnum {
    enum {
        ENUMERATOR0 = 0,
        ENUMERATOR1 = 1,
        ENUMERATOR2 = 2,
    } _val;
    constexpr MyEnum(decltype(_val) value) noexcept : 
        _val{value}
    {
    }
    constexpr explicit MyEnum(int value) noexcept : 
        _val{static_cast<decltype(_val)>(value)}
    {
    }
    [[nodiscard]] constexpr operator int() const noexcept
    {
        return _val;
    }
};

You can then use it like this:

MyType ar;
ar[MyEnum::ENUMERATOR0] = 3;
MyEnum e = MyEnum::ENUMERATOR2;
ar[e] = 2;

(godbolt).

Comments

0

I don't like the current behavior of enum class. In my case where I really need to enforce the enum name when specifying the enum value, I use the following code.

//Utilities.hpp
#define SETUP_ENUM_STRUCT_ASSIGNMENTS(EnumStruct, EType)        \
    EType val;                                                  \
    EnumStruct(){}                                              \
    EnumStruct(EType p_eVal):val(p_eVal) {}                     \
    operator EType () const { return val; }                     \
    void operator=(EType p_eVal) { val = p_eVal; }  

#define ENUM_STRUCT(EName, ...)                        \
struct EName {                                         \
    enum Type {                                        \
        __VA_ARGS__                                    \
    };                                                 \
    SETUP_ENUM_STRUCT_ASSIGNMENTS(EName, Type)         \
};


//main.cpp
int main(){

    ENUM_STRUCT( EFruit,   //<--this line is weird and looks unusual
        APPLE,             //    for enum definition, but this tool
        GRAPES,            //    does the work I need so it's OK for 
        ORANGE,            //    me despite the code weirdness.

        COUNT
    )

    std::array<int, EFruit::COUNT> listF;  //<--no need for any type-casting.

    listF[EFruit::APPLE] = 100;            //<--looks cleaner like enum class with
                                          //     no type restriction.

    return 0;
}

I use ENUM_STRUCT macro on special cases mainly for readability(which helps a lot in debugging). On most cases, I use the normal enum. I rarely use enum class because of the restrictions.

3 Comments

The restrictions of enum class are its point. I guess what some dislike is how they are all-or-nothing. This reinvents scoping while discarding strong typing. Now we can accidentally pass some int or other enum, get the implicit conversion of C-style enums, & end up doing something really wrong. That might be fine for you, & might even be fine for the OP, but it's not like you've objectively made something better than enum class for all contexts. For some people, having to cast for use as an index in implementation details is fine, as it means users are still subject to strong type
If someday, the developers of c++ language made an enum class values can be used as size in array definition or as index for array access without extra code such as static_cast, then I'll use enum class. Those 2 are the only reason why I can't use enum class for now. Currently they are syntax errors when enum class is used which surprised me when I 1st learned of that enum class.
Yeah, it'd be nice if the different features were opt-in, not all-or-nothing. I'm kinda torn: I can't tell (before searching discussions) if relying on enum values for indexing, arithmetic, etc. is OK or bad style... but it sure is easier than adding redundant maps, etc. The glibmm project and relatives, which wrap the C GLib et al., in the current dev version use something much like this, to get scoping but keep implicit conversion to int, at least for the subset of enums that allow extension with arbitrary values (I think they still use scoped enum class in other situations).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.