Вводное слово
У каждого программиста бывает желание велосипедостроения. Это случилось и у меня. Решил зарефакторить кусочек кода в приложении на текущей работе. Было приложение, которое выполняло работу в бесконечном цикле. И в этом цикле работа выполнялась, когда приложение было активно. То, что приложение активно в данный момент определялось через группу булевых значений. То есть каждую итерацию цикла приложение проверяла все эти булевы условия. Я подумал, что не лучше ли будет проверять одно значение вместо пачки. Так родился простой велосипед небольшого хранилища булевых условий в битах целочисленного типа.
В статье все примеры взяты из головы. Все совпадения случаны
Проблема и решение
Так вот. Как-то столкнулся я с кодом
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/
Добавить комментарий