Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use enum class values as part of for-loop?

I'm trying to create a deck of cards by iterating over the enums Suit and Rank (I know there's no great way to iterate over enums but I don't see an alternative). I did this by adding an enumerator enum_count to the end of each enum, whose value is meant to represent the length and end of the enum.

#include <vector>

using namespace std;

enum class Suit: int {clubs, diamonds, hearts, spades, enum_count};
enum class Rank: int {one, two, three, four, five, six, seven, eight,
                nine, ten, jack, queen, king, ace, enum_count};

struct Card {
    Suit suit;
    Rank rank;
};

class Deck{
    vector<Card> cards{};
    public:
        Deck();
};

Deck::Deck() {
    // ERROR ON THE BELOW LINE
    for (Suit suit = Suit::clubs; suit < Suit::enum_count; suit++) {
        for (Rank rank = Rank::one; rank < Rank::enum_count; rank++) {
            Card created_card;
            created_card.suit = suit;
            created_card.rank = rank;
            cards.push_back(created_card);
        };
    };
};

However, when I try to loop over the enum, the compiler doesn't like that I'm trying to increment the suit++ and rank++ in the for-loop, stating:

card.cpp|24|error: no ‘operator++(int)’ declared for postfix ‘++’ [-fpermissive]|
card.cpp|25|error: no ‘operator++(int)’ declared for postfix ‘++’ [-fpermissive]|

What is the best way to go about creating a deck of cards without throwing away the useful enum data structures?

like image 495
DBedrenko Avatar asked Feb 10 '16 10:02

DBedrenko


4 Answers

You can not use this with enum class. You have to use the old style enum.

If you insist you use them. I can suggest you a bad solution not to use (unless you won't tell anyone that I suggested it):

for (Rank rank = Rank::one; 
     static_cast<int>(rank) < static_cast<int>(Rank::enum_count); 
     rank = static_cast<Rank>(static_cast<int>(rank)+1)){
};
like image 85
Humam Helfawi Avatar answered Oct 17 '22 16:10

Humam Helfawi


With C++11, you can use a range-based for loop. All you need is to define an iterator class with operator++, operator!=, and operator* and to define begin or end as member functions or free functions.

Here is an example using an EnumRange class that includes an Iterator class and begin or end member functions. The class assumes that T is an enum class with contiguous values that start at 0 and end at MAX. The MAX declaration is used to avoid adding an invalid value, such as enum_count, to the enum. If the enum class does not define MAX, then the code will not compile.

template <class T>
struct EnumRange {
  struct Iterator {
    explicit Iterator(int v) : value(v) {}
    void operator++() { ++value; }
    bool operator!=(Iterator rhs) { return value != rhs.value; }
    T operator*() const { return static_cast<T>(value); }

    int value = 0;
  };

  Iterator begin() const { return Iterator(0); }
  Iterator end() const { return Iterator(static_cast<int>(T::MAX) + 1); }
};

This allows you to simply write:

enum class Suit {clubs, diamonds, hearts, spades, MAX=spades};
enum class Rank {one, two, three, four, five, six, seven, eight,
                 nine, ten, jack, queen, king, ace, MAX=ace};

for(const Suit s : EnumRange<Suit>())
    for (const Rank r : EnumRange<Rank>())
        cards.push_back({s,r});

An advantage of this approach is that it avoids the need to define/allocate a map or vector each time you want to iterator over an enum. Instead, the EnumRange::Iterator class stores a single integer and any changes to the enum are automatically supported. Also, since we have defined operator* to cast the integer to the enum type T, we know that the variable type of the range-based for loop is the enum.

All together, the result is the easy-to-read expression for(MyEnum s : EnumRange<MyEnum>()).

like image 20
eric Avatar answered Oct 17 '22 17:10

eric


I would recommend doing something different. Create a vector of Suit and one to Rank, and loop over them using the power of STL

const std::vector<Suit> v_suit {Suit::clubs, Suit::diamonds, Suit::hearts, Suit::spades};

const std::vector<Rank> v_rank {Rank::one, Rank::two, Rank::three, Rank::four, Rank::five, 
                          Rank::six, Rank::seven, Rank::eight, Rank::nine, Rank::ten, Rank::jack, 
                          Rank::queen, Rank::king, Rank::ace};

Yes, you have to type them twice, but this permits you to use whatever values you want for them (ie. not consecutive), not use awkward stuff like enum_count (What card do you want? Give me a diamonds enum_count!!), no need for casting, and use the iterators provided to std::vector.

To use them:

for(const auto & s : v_suit)
    for (const auto & r : v_rank)
        cards.push_back({s,r});
like image 43
hlscalon Avatar answered Oct 17 '22 17:10

hlscalon


Additional answer in response to old_mountain's answer:

You can in some cases prevent that you forget to add new values to your list by using fixed arrays. The main problem with this is that the initializer accepts less arguments than specified but you can work around this:

template<typename T, typename...Args>
struct first_type
{
    using type = T;
};

template<typename... Args> 
std::array<typename first_type<Args...>::type, sizeof...(Args)> make_array(Args&&... refs) 
{
    return std::array<typename first_type<Args...>::type, sizeof...(Args)>{ { std::forward<Args>(refs)... } };
}

I found the inspiration to make_array in this question, but modified it: How to emulate C array initialization "int arr[] = { e1, e2, e3, ... }" behaviour with std::array?

What it does is to use the first argument to find out what type the std::array shall be of and the number of arguments to get the real size. So the return type is std::array<firstType, numArgs>.

Now declare your lists like this:

const std::array<Suit, (size_t)Suit::enum_count> SuitValues = 
    make_array(Suit::clubs, Suit::diamonds, Suit::hearts, Suit::spades);

const std::array<Rank, (size_t)Rank::enum_count> RankValues = 
    make_array(Rank::one, Rank::two, Rank::three, Rank::four, 
               Rank::five, Rank::six, Rank::seven, Rank::eight,
               Rank::nine, Rank::ten, Rank::jack, Rank::queen, 
               Rank::king, Rank::ace);

When you add a value to the array your enum_count or whatever value you are using as delimiter will change and therefore the assignment from make_array will fail as the sizes of both std::arrays differ (which results in different types).

Example:

If you just add a new Suit, let's say hexa

enum class Suit : int { clubs, diamonds, hearts, spades, hexa, enum_count };

The compiler will fail with:

cannot convert from 'std::array<T,0x04>' to 'const std::array<Suit,0x05>'

I have to admit that I am not 100% happy with this solution as it requires a pretty ugly cast to size_t in the array declaration.

like image 22
Simon Kraemer Avatar answered Oct 17 '22 15:10

Simon Kraemer