Итерируемся по enum'ам в C++
В этой статье я хочу поделиться простым и очевидным трюком в 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);
}