6

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?

janekb04
  • 4,304
  • 2
  • 20
  • 51
ltsstar
  • 832
  • 1
  • 8
  • 24
  • I just use a macro: #define INDEX(a) static_cast(a) – Andrew Truckle Dec 25 '17 at 22:45
  • If you don't mind using `enum MyEnum`, i.e. without the `class` in there, there should be no problem. – R Sahu Dec 25 '17 at 22:46
  • Is there a reason for using `enum class`? What is the use-case? What problem is it supposed to solve? – Some programmer dude Dec 25 '17 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. – ltsstar Dec 25 '17 at 22:59
  • Either use legacy `enum` or put the `array` in a class for which you will overload `operator[]` or add a function that you call to access desired item or use a `std::map`. – Phil1970 Dec 25 '17 at 23:26
  • The entire purpose of enum class is to avoid the implicit conversion, just use plain enums – Passer By Dec 26 '17 at 02:19
  • @AndrewTruckle Please dear god, no. – Passer By Dec 26 '17 at 02:19
  • I mean the use of *scoped* enumeration more specifically. Why enum ***class***? – Some programmer dude Dec 26 '17 at 09:39
  • 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. – ltsstar Dec 26 '17 at 10:00

5 Answers5

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
} 
jordi
  • 336
  • 4
  • 7
  • 2
    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. – Remy Lebeau May 18 '21 at 18:13
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.

unholytony
  • 113
  • 4
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
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...

Will59
  • 1,430
  • 1
  • 16
  • 37
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).

janekb04
  • 4,304
  • 2
  • 20
  • 51
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.

acegs
  • 2,621
  • 1
  • 22
  • 31
  • 1
    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 `enum`s, & 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 – underscore_d Sep 29 '18 at 09:27
  • 1
    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`. – acegs Sep 30 '18 at 10:13
  • 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 `map`s, 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 `enum`s that allow extension with arbitrary values (I *think* they still use scoped `enum class` in other situations). – underscore_d Sep 30 '18 at 10:28