четверг, 5 ноября 2015 г.

Безопасные и удобные битовые маски

Регулярно встречается задача в которой есть небольшое количество фундаментальных состояний и при этом возможны их комбинации. Идеальным по удобству в этом случае является использование битовых масок. Заводим 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 сдлеть невозможное возможным.


Комментариев нет: