Регулярно встречается задача в которой есть небольшое количество фундаментальных состояний и при этом возможны их комбинации. Идеальным по удобству в этом случае является использование битовых масок. Заводим enum каждое из его значений делаем равным степени двойки и используем побитовые И и ИЛИ, но...
Сама переменная имет численный тип и глядя на неё совершенно неочевидно, что её значения это комбинации битов описанных вон тем перечислением. Эти знания оказались не записаны в коде и должны как-то иначе быть доведены до коллег по команде. Из за этого в современном мире C++ многие предпочитают std::vector<MyEnum> либо std::set<MyEnum>, что с учётом ограниченных удобств предоставляемых STL контейнерами, в коде, всё же, проигрывает по читаемости битовым маскам.
Но... Выход существует. Немного подумав, можно написать следующий шаблонный класс:
template<typename E, typename T = uint64_t> class Bitmask { public: constexpr Bitmask(E e): val(mask(e)) {} constexpr Bitmask(const Bitmask &other) = default; constexpr Bitmask & operator= (const Bitmask &other) = default; constexpr Bitmask operator| (Bitmask rhs) const { return Bitmask(val | rhs.val); } constexpr Bitmask operator| (E e) const { return Bitmask(val | mask(e)); } constexpr Bitmask operator& (Bitmask rhs) const { return Bitmask(val & rhs.val); } constexpr Bitmask operator& (E e) const { return Bitmask(val & mask(e)); } constexpr operator bool () const { return val != 0; } private: constexpr Bitmask(T t): val(t) {} constexpr static T mask(E e) { return T(1) << static_cast<T>(e); } private: T val; }; template<typename E, typename T = uint64_t> constexpr Bitmask<E, T> operator| (E lhs, Bitmask<E, T> rhs) { return Bitmask<E, T>(lhs) | rhs; } template<typename E, typename T = uint64_t> constexpr Bitmask<E, T> operator& (E lhs, Bitmask<E, T> rhs) { return Bitmask<E, T>(lhs) & rhs; }
enum с которым хочется использовать такой класс можно заводить не присваивая никаких значений отдельно взятым элементам. При этом желательно определить оператор побитового или от двух значений этого перечисления, чтобы было максимально комфортно пользоваться тем что получилось:
enum class Perm {Read, Write, Exec}; constexpr Bitmask<Perm> operator| (Perm lhs, Perm rhs) { return Bitmask<Perm>(lhs) | Bitmask<Perm>(rhs); }
Ну и, собствнно, можно совершенно просто и удобно делать вот так:
Bitmask<Perm> mask = Perm::Read | Perm::Write; if (mask & Perm::Exec) std::cout << "Executable" << std::endl;
Единственное, что не получается сделать в этом подходе, так это корректно написать оператор инверсии. Просто выполнив ~val мы взведём биты, которые невозможно взвести используя элементы enum'а и нарушим работу опертора приведения к bool. Правильным решением была бы следующая реализация инверсии:
template<typename E> constexpr std::initializer_list<E> all_elements(); template<typename E, typename T = uint64_t> class Bitmask { public: ... constexpr Bitmask operator~ () const { return ~val & allowed(); } private: constexpr static T allowed() { T res; for (E e: all_elements<E>()) res |= mask(e); return res; } ... };
Единственная проблема, это реализация функции all_elements. К сожалению в современном C++ она невозможна, а посему ждём и верим что SG7 успеет определиться с тем как такие вещи делать до выхода C++17. Например N4428 уже позволяет при помощи std::index_sequence и variadic template сдлеть невозможное возможным.
Комментариев нет:
Отправить комментарий