Контейнер ConditionalBitset — небольшое хранилище для условий выполнения

от автора

Вводное слово

У каждого программиста бывает желание велосипедостроения. Это случилось и у меня. Решил зарефакторить кусочек кода в приложении на текущей работе. Было приложение, которое выполняло работу в бесконечном цикле. И в этом цикле работа выполнялась, когда приложение было активно. То, что приложение активно в данный момент определялось через группу булевых значений. То есть каждую итерацию цикла приложение проверяла все эти булевы условия. Я подумал, что не лучше ли будет проверять одно значение вместо пачки. Так родился простой велосипед небольшого хранилища булевых условий в битах целочисленного типа.

В статье все примеры взяты из головы. Все совпадения случаны

Проблема и решение

Так вот. Как-то столкнулся я с кодом

bool hasFocus     { false }; bool hasWindow    { false }; bool isInitialized{ false }; bool isVisible    { false }; 

Который дальше имел такую конструкцию:

bool isActive() const {     return isInitialized && hasFocus && isVisible && hasWindow; } 

Отсутствие constexpr не имеет значения для статьи. Это просто пример.

И подумалось мне, а почему так? Для чего проверять каждый раз пачку булевых значений, когда в принципе можно только одно. isActive например. И тоже булево. С точки зрения оптимизации, если isActive используется для проверки в цикле, где задержки нежелательны, проверить одно значение быстрее, чем 4. Да и те, что я предоставил в пример, это лишь пример. Условий может быть куда больше. Я видел как-то код с 12 условиями.

Так вот, почему бы не использовать одно значение? Запретов как бы нет. Но возникает другой вопрос. А как удостовериться, что все условия соблюдены? И полученная истина в isActive явно указывает на то, что и hasFocus, и hasWindow, и isInitialized, и isVisible истины? То есть можно написать методы setHasFocus, setHasWindow, seIsVisible и setIsInitialized. И в каждом из них проверять наличие всех условий. Например:

void setHasFocus(bool value) {     hasFocus = value;     isActive = isInitialized && hasFocus && isVisible && hasWindow; } 

Но это лишние проверки, и потенциальные ошибки, если надо добавить ещё условия. И кто-то легко может для своего удобства не через метод «погасит» условие, а напрямую, через переменную.

Я решил, что здесь хорошо подойдут битовые поля. Но использовать std::bitset я не хотел, так как это немного не то. Вот что внутри у std::bitset clang’а:

__storage_type __first_[_N_words]; 

Работа с массивом. И вот это вот всё. Старые добрый битовые операции — всё, что нужно. Почему нет? Берём целочисленный тип, и принимаем, что 0 — это истина. То есть, если все биты памяти этого типа стоят в 0, то значит все условия соблюдены. Если есть хотя бы один бит, то не все условия соблюдены. Логично? Логично. Выставляя биты — устанавливаются условия. Убирая биты — условия выполняются. Даже текстом выглядит просто.

Теперь вопрос в том, как выставлять биты? Здесь поможет степень двойки, битовый сдвиг и перечисления.

enum class AppIsActiveConditions : uint8_t {     NONE        = 0,     INITIALIZED = (1 << 0),     HAS_FOCUS   = (1 << 1),     HAS_WINDOW  = (1 << 2),     IS_VISIBLE  = (1 << 3), }; 

Пример простой, но уже надеюсь понятно что к чему. Внутренний тип перечисления uint8_t нужен для создания числа, в котором поместятся все условия. По умолчанию у перечислений в C++ внутренний тип int. Можно по сути использовать любой целочисленный. Со знаком или без — это не важно. Главное, чтобы все условия имели положительные и не повторяющиеся значения степени двойки. Условие NONE с нулём вспомогательное. Оно никак не используется. Но применение и ему можно найти, если захотеть.

Объявление переменной хранилища выглядит так:

ConditionalBitset <AppIsActiveConditions> isActive {     AppIsActiveConditions::INITIALIZED,     AppIsActiveConditions::HAS_FOCUS,     AppIsActiveConditions::HAS_WINDOW,     AppIsActiveConditions::IS_VISIBLE }; 

Переменная isActive хранит в себе биты условий. И не равно 0. Далее остаётся только указывать, какие условия выполнились. Или заново установить условия, если условие перестало быть истинным. Делается это через методы

isActive.reach(AppIsActiveConditions::INITIALIZED); // достигли условия isActive.lose(AppIsActiveConditions::INITIALIZED);  // условие потеряли 

Оба метода возвращают булево для того, чтобы проинформировать. true возвращается, когда методы выполнили работу. false же означает:

  • для метода reach, что при достижении условия — условие либо уже было достигнуто, либо не устанавливалось изначально.

  • для метода lose, что при потере условия — условие либо ещё не было достигнуто, либо было установлено ранее.

Немного заумно, но так проще понять.

Проверить наличие условия можно через метод

isActive.isReached(AppIsActiveConditions::INITIALIZED); 

Через него можно заменить и запросы к старым булевым значениям.

Осталось только показать код класса.
(Спойлер в markdown почему-то не работает. Оставлю так, портянкой — для объёму)

Код класса

// Предварительное проверочное объявление класса, // чтобы случайно не ввести другие типы.  // Потому что нам нужны только перечисления template <typename Type, bool = std::is_enum<Type>::value> class ConditionalBitset;  // Основное тело класса template <typename EnumType> class ConditionalBitset <EnumType, true> final {     // Выделяется внутренний тип перечисления     using EnumUnderlyingType = typename std::underlying_type<EnumType>::type;  public:     // Указываю, что нужно использовать только этот конструктор      template <typename...Args>     explicit constexpr ConditionalBitset (Args&&...args) noexcept     {         ((add(std::forward<Args>(args))),...);     }      // Преведение типа к булеву для простоты проверки     // Сравнение с нулём для наглядности, можно указать `!value`     constexpr operator bool() const noexcept { return value == 0; }      constexpr bool isReached(EnumType condition) const noexcept     {         return !has(condition);     }      constexpr bool lose(EnumType condition) noexcept     {         if (has(condition))         {             return false;         }          add(condition);         return true;     }      constexpr bool reach(EnumType condition) noexcept     {         if (!has(condition))         {             return false;         }          remove(condition);         return true;     }  private:     // Вспомогательные приватные методы тоже для наглядности      constexpr void add(EnumType condition) noexcept     {         value |= static_cast<EnumUnderlyingType>(condition);     }      constexpr bool has(EnumType condition) const noexcept     {         return (value & static_cast<EnumUnderlyingType>(condition));     }      constexpr void remove(EnumType condition) noexcept     {         value ^= static_cast<EnumUnderlyingType>(condition);     }      // Целое число, как битовое поле     EnumUnderlyingType value;   }; 

Просто и наглядно.

Минусы решения

И в при таком решении есть свои минусы.

Основной минус, что нельзя динамически добавлять условия. Но мне это и не было нужно.

Так же нет проверки на одинаковые значения условий. Поэтому важным критерием является не повторяющиеся значения для условий.

Не менее значительный минус в том, что теперь в дебаге сложнее смотреть какие условия уже выполнились, а какие ещё нет. Требуется складывать значения условий в уме.

И ещё — нельзя «выключить» условие, можно только либо «погасить» его в переменной где-то в коде, либо удалить его из списка инициализации переменной.

На самом деле минусов куда больше. Я указал только часть

Benchmarks

Их нет. Этот велосипед — это велосипед. И не рассматривался, как конечное решение для прода. А для статьи для Хабра — самое то.


На этом всё. Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/824812/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *