Итерируемся по enum'ам в C++

5b3b0816d7da312c7b0b8d19e5dcfa33

В этой статье я хочу поделиться простым и очевидным трюком в C++, о котором, не смотря на его простоту и очевидность, как оказалось, не все знают.

Предположим, у вас есть enum class, и вы хотите пройтись по его элементам, то есть вызвать какую-то функцию для каждого из значений этого enum’а. Это может быть полезно, ну… как пример, если элементы enum’а представляют параметры (ключи для значений), которые вы можете получить от какой-нибудь сущности или сервиса, и вам нужно обработать их все сразу по-очереди, например, для сериализации или логирования.

В языке C# для этой задачи есть метод Enum.GetValues(typeof(T)), который возвращает коллекцию всех значений этого enum’а. В C++ такого метода нет, но мы можем попробовать реализовать что-то подобное.

Итак, возьмем обычный и всеми любимый range-based for loop:

for (auto i : some)
{
  ...
}

Cppreference нам говорит, что для использования цикла range-based for с любым типом, который не является массивом, этот тип должен иметь методы begin () и end (), которые возвращают что-то, поддерживающее операторы ++ (для перехода к следующему значению) и * (для получения значения), то есть итератор.

Исходя из этого, мы приступаем к созданию нашего шаблонного класса, в который мы сможем обернуть любой enum. Первым делом начнем с итератора:

template 
class Enum
{
public:
  class Iterator
  {
  public:
    constexpr explicit Iterator(std::underlying_type_t value)
      : m_value(value)
    {}
    constexpr T operator*() const
    {
      return static_cast(m_value);
    }
    constexpr void operator++()
    {
      ++m_value;
    }
    constexpr bool operator!=(Iterator rhs) const
    {
      return m_value != rhs.m_value;
    }
  private:
    std::underlying_type_t m_value;
  };
};

Enum «под капотом» представляет собой числовое значение, поэтому мы можем легко преобразовать его в числовой тип и обратно (это будет не обязательно int, поэтому используем std: underlying_type). Единственное условие — элементы в enum должны быть последовательными, без пропусков.

С итератором разобрались, но как же реализовать функции begin () и end ()?

Для этого нам потребуется немного подшаманить над самим нашим енамом. Допустим, вы хотите создать перечисление с элементами One, Two и Three.

Объявим его так:

enum class MyType
{
  Begin,
  One = Begin,
  Two,
  Three,
  End
};

Теперь мы легко можем получить доступ к первому элементу перечисления через псевдоним MyType: Begin (который численно соответствует элементу One), а также проверить, достигли ли мы конца перечисления путём сравнения с MyType: End.

После этого код методов begin () и end () умещается буквально в пару строчек:

template 
constexpr typename Enum::Iterator begin(Enum)
{
  return typename Enum::Iterator(static_cast>(T::Begin));
}

template 
constexpr typename Enum::Iterator end(Enum)
{
  return typename Enum::Iterator(static_cast>(T::End));
}

И мы легко можем итерироваться по всем элементам любого enum’а, содержащего Begin и End, используя нашу обертку:

for (auto item : Enum)
{
  // используем item:
  // код здесь внутри будет вызван три раза: для One, Two и Three
}

И бонусом можно добавить метод size () для нашего класса Enum, вдруг пригодится:

static constexpr std::size_t size()
{
  return static_cast>(T::End) - static_cast>(T::Begin);
}

© Habrahabr.ru