2

Say we have an enum type foo which we want to use to index an array arr of static size.

If we want to use an enum class for this, we could try it like this:

enum class foo
{
    a,
    b,
    c,
    count
};

std::array<T, static_cast<int>(foo::count)> arr;

However, the count field is a hack. Can we obtain the number of fields of foo in a more elegant way?

In any case, what's really bad is that we need to access the array using a static_cast as well: arr[static_cast<int>(foo::a)].

Of course we could write a custom "at" function (see https://www.fluentcpp.com/2019/01/15/indexing-data-structures-with-c-scoped-enums/) or provide an "enum_array" class (see https://stackoverflow.com/a/55259936/547231), but both solutions are somehow complicated and we might better give up and use a simple std::array<T, int> instead ...

However, it's way more intuitive to read arr[foo::a] instead of arr[0], where we always need to remember what's the meaning of the index 0 in the latter.

Can we do better?

0xbadf00d
  • 17,405
  • 15
  • 67
  • 107
  • 1
    really bad? If you want to use the enum as index why not a plain old `enum` ? `enum class` main effect is that you need a cast, if you dont want that you dont have to – 463035818_is_not_an_ai Jan 07 '20 at 20:18
  • 3
    Use a map/unoredered_map with the enum as the key type? – NathanOliver Jan 07 '20 at 20:19
  • @NathanOliver Sure, but won't this end in a look-up for each array access? Seems not worth the price. – 0xbadf00d Jan 07 '20 at 20:20
  • @0xbad that lookup will be [on average O(1)](https://en.cppreference.com/w/cpp/container/unordered_map/operator_at) with an unordered_map. And especially if your enum's are only 3 items long like the example, even an O(n) solution probably wouldn't make a reasonable difference. Maybe you could tell us a little more about what you're trying to do that makes the runtime on this operation so important? – scohe001 Jan 07 '20 at 20:23
  • Should `arr` be publically accessible? I usually only use this idiom with member arrays, where I don't want the caller to have to work with indices. Then I only have the `static_cast` in getters and setters, making the "uglyness" very contained – UnholySheep Jan 07 '20 at 20:25
  • @scohe001 As written in the first comment, we could still use an `enum` instead. The only reason why I would prefer an `enum class` here is that I don't want the enumerator to leak names into the scope. – 0xbadf00d Jan 07 '20 at 20:25
  • @UnholySheep No, I'm using it as a member field as well, but I think it's then still better to use an `enum` instead to keep the code inside the class simple. – 0xbadf00d Jan 07 '20 at 20:27
  • @0xbad I'm confused how this relates to my comment...you said you were worried about the runtime of a map lookup. Why? Why is an on average constant runtime not good enough for you? – scohe001 Jan 07 '20 at 20:28
  • 2
    @scohe001 Why should I add an overhead (even it is of constant complexity) just to get such a little benefit over an ordinary `enum`? – 0xbadf00d Jan 07 '20 at 20:29
  • The theoretical complexity is in this example not that important. But the basic overhead we get from using a map in the first place (in the form of cache misses when iterating, an extra level of indirection, etc...) is pretty relevant here. I would go so far and say that an O(n) find on an array this small is faster than the O(1) lookup in a map of the same size. If it is in the end really relevant for the performance of your program is something you should find out by performing your xode. – n314159 Jan 07 '20 at 20:48

2 Answers2

3

No, not really.

There are many proposals that enable static reflection of enum values. None are in C++ yet.

I mean you could do:

namespace foo {
  enum value {
    a,b,c,count
  };
}

then the to int conversion is implicit, and you don't pollute the contained namespace.

The solution here is pretty close to 0 overhead and lets you use enums (and only enums) as keys to [].

So you get:

enum_array<foo, T> arr;

and arr behaves like you want it to.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Are you sure it only accepts enums as keys? The `using base::operator[]` should expose the `size_t` version of this operator. (And since it is a `struct` the inheritance is public and as such these operators should already be exposed to start with) – n314159 Jan 07 '20 at 20:44
  • @n314159 Ah, commented it out in the original post. With the using gone, standard lookup won't find the `base[]` operator. Mentioned you can comment it back in if you want `[size_t]` to work. – Yakk - Adam Nevraumont Jan 07 '20 at 21:00
  • I think it would be also appropriate to add a warning on the dangers of inheriting publically from classes without virtual destructor. – n314159 Jan 07 '20 at 21:07
  • 1
    @n314159 In 999/1000 cases, someone allocating a raw `std::array` (or an `enum_array`) on the heap is making a mistake *already*. So I don't think it is worth the words here. What more, while deleting-through-base is undefined by the standard, every compiler's resulting UB generated actually does the right thing when there is an "empty" derived extension. Between the two, producing an actual problem in actual production this specific case requires a really contrived situation. So I'll pass. – Yakk - Adam Nevraumont Jan 07 '20 at 21:11
2

As a partial solution you can define

constexpr std::underlying_type_t<foo> operator*(foo f) {
    return static_cast<std::underlying_type_t<foo>>(f);
}

and then write

int bar(std::array<int, *foo::count>& arr) {
    return arr[*foo::b];
}

Demo

Evg
  • 25,259
  • 5
  • 41
  • 83