Контейнер 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/


Интересные статьи

Интересные статьи

Мы перевели статью, где подробно рассматривается использование модуля Ansible Shell и различные способы выполнения удалённых команд на узлах в рамках работы по автоматизации. В статье рассматриваются ...
Привет, Хабр!Анализ исходного кода - давно зарекомендовавшая себя практика для выявления отклонений до выхода приложения на рынок. Проверка на уязвимости, program understanding, поиск логических ошибо...
Привет, Хаброжители! Во многих организациях приложения работают в нативных облачных средах, обеспечивая масштабируемость и отказоустойчивость с помощью контейнеров и средств координации....
Революция искусственного интеллекта не грядёт, она уже здесь и требует от компаний переосмысления бизнес-процессов, чтобы максимально использовать возможности искусственного интеллект...
Друзья, привет. В статье я расскажу как собрать решение для быстрого старта вашей разработки из standalone Java приложения, брокера и Zookeeper’а Kafka, поднятых в отдельных Docker ко...