Контейнер 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 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 ::value>
class ConditionalBitset;
// Основное тело класса
template
class ConditionalBitset final
{
// Выделяется внутренний тип перечисления
using EnumUnderlyingType = typename std::underlying_type::type;
public:
// Указываю, что нужно использовать только этот конструктор
template
explicit constexpr ConditionalBitset (Args&&...args) noexcept
{
((add(std::forward(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(condition);
}
constexpr bool has(EnumType condition) const noexcept
{
return (value & static_cast(condition));
}
constexpr void remove(EnumType condition) noexcept
{
value ^= static_cast(condition);
}
// Целое число, как битовое поле
EnumUnderlyingType value;
};
Просто и наглядно.
Минусы решения
И в при таком решении есть свои минусы.
Основной минус, что нельзя динамически добавлять условия. Но мне это и не было нужно.
Так же нет проверки на одинаковые значения условий. Поэтому важным критерием является не повторяющиеся значения для условий.
Не менее значительный минус в том, что теперь в дебаге сложнее смотреть какие условия уже выполнились, а какие ещё нет. Требуется складывать значения условий в уме.
И ещё — нельзя «выключить» условие, можно только либо «погасить» его в переменной где-то в коде, либо удалить его из списка инициализации переменной.
На самом деле минусов куда больше. Я указал только часть
Benchmarks
Их нет. Этот велосипед — это велосипед. И не рассматривался, как конечное решение для прода. А для статьи для Хабра — самое то.
На этом всё. Спасибо за внимание!