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

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

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

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

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

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; 

};

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

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

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

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

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

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

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

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

upd.
В методе remove используется оператор XOR (^), который имеет особенность - он возвращает разницу между двумя операндами. Он может как удалить бит, если он был, так и установить его, если его не было. Это может стать проблемами в будущем, если вызывать метод remove повторно на одно и тоже значение.

Более идиоматическое «удаление» выглядит так

constexpr void remove(EnumType condition) noexcept
{
    value &= ~(static_cast<EnumUnderlyingType>(condition));
}

Оно всегда выполняет удаление без непредвиденных эффектов при неправильном использовании. Но здесь уже две операции: логическое отрицание и логическое умножение вместо одной.

Benchmarks

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


upd.

Чуть более детальное описание и немного теории

Не вдаваясь в более детальные подробности укажу, что память храниться, как набор битов. В обычной переменной типа int для 32-битной системы 4 байта, или 32 бита, так как в одном байте 8 бит.
Тип int в памяти можно представить так

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 

Число 3 будет выглядеть так (это всё те же 4 байта)

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 

Выше в статье было указано такое выражение (1 << 0). Это выражение - битовый сдвиг влево (оператор <<), относящийся к битовым операция с памятью. В более общем виде, это выражение будет выглядеть так n << m. С помощь него можно сдвинуть некую память n на некое число битов m влево. В примере 1 << 0 число 1 сдвигается на 0 битов. И по сути результат выражения равен 1. Почему тогда не написать просто 1 вместо 1 << 0? Это сделано специально для наглядности. Ведь далее в коде статьи показаны и другие выражения смещения 1 << 1, 1 << 2, 1 << 3. И вместе они уже наглядно показывают, что производится смещение числа 1 на некоторое число битов.

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

Таким образов формируется перечисление, которое используется как набор Условий, который будут «храниться» в Контейнере.

Каждое Условие, по задумке, это целочисленное не повторяющееся положительное число равное числу, полученному из степень двойки, то есть 2x. А почему нужно число именно степени двойки? Потому что особенность всех чисел степени двойки в том, что их представление в двоичной системе исчисления содержит только 1 бит равный 1.

Например число 4 в одном байте выглядит так

0 0 0 0 0 1 0 0 

А число 32 - так

0 0 1 0 0 0 0 0 

Число 1, тоже степени двойки, только степень равно нулю (20 = 1). И представляется в памяти так

0 0 0 0 0 0 0 1 

И если сдвинуть память занимаемую числом 1 влево на два бита (1 << 2), то получим

0 0 0 0 0 1 0 0 

А это уже число 4. Таким образом, сдвигая память числа 1 влево, можно получать нужное число степени двойки.

Добавление Условий в Контейнер

И теперь представьте, что каждый бит хранимого в Контейнере числа можно использовать, как отдельное булево значение, где 1 это true (ИСТИНА), а 0 это false (ЛОЖЬ). Что, собствено, и есть на самом деле. Тип bool в языке C++ - это синтаксический сахар, чтобы удобнее было работать с таким типом. В языке C, к примеру, нет типа bool. Его «эмулирую» через целочисленный тип (обычно int).

Примем, что каждое Условие - это не повторяющееся число степени двойки. Остаётся только Условия «вложить» в Контейнер. Для этого используется битовый оператор | (логическое сложение, OR, ИЛИ). Выражение будет иметь вид n | m. Для примера «сложим» числа 32 и 4

0 0 1 0 0 0 0 0  // 32
                 // |
0 0 0 0 0 1 0 0  // 4
                 // =
0 0 1 0 0 1 0 0  // 36

Получится число 36.

В статье в коде класса оператор | применяется в приватном методе add. Метод add, как бы добавляет проверяемое Условие в Контейнер. Метод приватный и применяется в публичных конструкторе и методе lose. Конструктор указан как «явный» (explicit)., чтобы при объявлении переменной сразу выставить нужные Условия для проверки. Такова задумка изначально. Других конструкторов не предусмотрено. В методе 'lose', по задумке, выставляется Условие, которое перестало быть истинным. То есть «потеряли» его. Отсюда и название lose.

«Удаление» Условий из Контейнера

Для «отключения» битов в числе, хранимом в классе, применяется оператор ^ (логическое вычитание, исключающее ИЛИ, XOR). Выражение будет иметь вид n ^ m. Оператор ставит в 0 похожие биты числа, а не похожие в 1. «Вычтем» число 32 из числа 36.

0 0 1 0 0 1 0 0  // 36
                 // ^
0 0 1 0 0 0 0 0  // 32
                 // =
0 0 0 0 0 1 0 0  // 4

Получим число 4.

В статье в коде класса оператор ^ применяется в приватном методе remove. Метод remove, удаляет проверяемое Условие из числа, хранимого в классе. Метод приватный и применяется только в публичном методе reach. Название reach выбрано для описание того, что мы как бы «достигли» проверяемого Условия.

Результат работы

В итоге, после того, как все Условия были «достигнуты», в Контейнере должно остаться число 0. И именно оно принято за конечный результат. Для того, чтобы оставить логику булево значения, то есть, чтобы можно было объект класса использовать в ветвениях и циклах, в классе был определён оператор operator bool() для неявного приведения хранимого в классе числа к булеву значению.


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

Комментарии (51)


  1. avbochagov
    26.06.2024 14:00
    +4

    Мда... странное у вас представление о простоте.


    1. V1tol
      26.06.2024 14:00
      +1

      Это довольно распространённый способ оптимального хранения состояний. Настолько, что некоторые языки, как Swift например, имеют поддержку в стандартной библиотеке. Уверен, что и на C++ есть популярные и протестированные в бою библиотеки, чтобы не городить велосипеды самостоятельно.


    1. AnimeSlave Автор
      26.06.2024 14:00

      Оно на самом деле просто. Это битые поля. Если нужны более детальные описания что-то, то можете спросить, я попытаюсь ответить.


      1. avbochagov
        26.06.2024 14:00
        +1

        Как по мне - так исходный код проще и наглядней. Сопровождать его в разы лучше, чем это нагромождение шаблонной магии.

        Хотя как упражнение для ума... может быть интересно.


        1. AnimeSlave Автор
          26.06.2024 14:00

          Булевы переменные в исходном варианте несомненно нагляднее, а вот с простотой уже не всё так однозначно. Это в примере все переменные в одном месте. А представьте себе разбросанное по классу, или даже классам, эти переменные. Тот же метод isActive может быть совсем в другом месте. И уже сопровождение не такое удобное.

          Можно обойтись без шаблонной магии. И вообще без класса, используя только приватные методы. Шаблонная магия здесь только для проверок на этапе компиляции. Чтобы случайно не косячить


          1. avbochagov
            26.06.2024 14:00

            Повторюсь еще раз - как упражнение для ума - просто прекрасное решение. И, на мой взгляд, выполнено очень красиво.

            Но использовать его в продакшен коде - очень сомнительное решение.
            Причин несколько:

            • вместо кода, который определял набор флагов у нас теперь объект со всеми вытекающими последствиями.

            • при анализе ошибок мне теперь надо проанализировать не только набор условий, но и код создания объекта (а это шаблон, и не простой), и методы которые выдают решение условия.

            • ну и вишенка на торте - неявное ограничение на enum. В статье описано, что элементы enum должны быть нумерованы степенью 2-ки, но в коде это никак не отразить и на этом можно словить феерический баг.

            Вот основные причины, по которым я считаю это решение переусложнённым.

            P.S. И заметьте, я не говорю про увеличение времени компиляции, расход памяти и прочие радости.


  1. naviUivan
    26.06.2024 14:00

    Даже для велосипеда перебор получился. Почему не реализовать легкую обертку над std::bitset или использовать его напрямую? Или я чего-то не понял в условиях задачи?


    1. AnimeSlave Автор
      26.06.2024 14:00

      Основное условие упростить и, возможно, ускорить работу. std::bitset под капотом имеет массив. Это уже не быстрее группы коньюкций (&&), если нужно проверить все «биты». И не меньше по размеру, чем одна целочисленная переменная, если важно потребление памяти


  1. NeoCode
    26.06.2024 14:00
    +6

    В какую же хрень превратился современный С++ :( На такую простую конструкцию столько писанины, не делающей ничего полезного и нужной лишь для формального выполнения многочисленных требований и ограничений "правильного" с точки зрения последних стандартов кода.

    Кстати вот здесь

    constexpr void remove(EnumType condition) noexcept
    {
        value ^= static_cast<EnumUnderlyingType>(condition);
    }

    не remove, а инверсия. Удаление это value &= ~static_cast❮EnumUnderlyingType❯(condition);

    Ну а вообще есть битовые поля.

    struct Conditions
    {
        unsigned Initialized : 1;
        unsigned HasFocus    : 1,
        unsigned HasWindow   : 1,
        unsigned Visible     : 1,
    };

    И далее все просто

    Conditions c;
    c.Initialized = 1;
    if(c.HasFocus) foo();
    if(c.Visible && !c.HasWindow) bar();

    При желании можно еще завернуть в union и получить доступ ко всем полям сразу.


    1. seamant
      26.06.2024 14:00

      Дополню сразу

      typedef union{
        struct{
          uint8_t initialized : 1;
          uint8_t has_focus   : 1;
          uint8_t has_window  : 1;
          uint8_t visible     : 1;
          uint8_t reserved    : 4;
        }
        uint8_t as_uint8;
      } CONDITIONS;

      И далее можно выполнить одну проверку всех условий разом

      //0x0F если для всех полей считаем 1 корректным значением
      constexpr uint8_t CORRECT_ALL_CONDITIONS_VALUE = 0x0F;
      
      inline bool IsAllConditionsCorrect(CONDITIONS cond){
        return cond.as_uint8 == CORRECT_ALL_CONDITIONS_VALUE;
      }

      Самое главное - проверять размер union'ов


      1. eao197
        26.06.2024 14:00
        +3

        А вы уверены, что в C++ эта штука не является UB?
        ЕМНИП, обращение к неактивному элементу union-а в C++ -- это UB.

        И еще я не уверен, что в C++ есть гарантия того, что распределение битовых полей идет от самых младших битов к старшим. Вроде как ничего не мешает распределять и от старших битов к младшим и это зависит от платформы и компилятора.


        1. seamant
          26.06.2024 14:00

          Да, вы во всём правы. Для С++ это всё действительно характерно. Надо свыкаться с тем, что С++ и С уже очень сильно разошлись


        1. NeoCode
          26.06.2024 14:00

          Тут все сводится к вопросу шашечки нам нужны или ехать:) На практике компиляторов по пальцам руки пересчитать (гцц, мсвц, шланг, несколько штук для микроконтроллеров..., и обычно они все работают одинаково). А в большинстве случаев проект вообще заточен под какой-то единственный конкретный компилятор.


          1. eao197
            26.06.2024 14:00
            +1

            Тут все сводится к вопросу шашечки нам нужны или ехать

            Похоже, вы сейчас пытаетесь мне объяснить, что UB в C++ном коде -- это нормально? Ну ну, ну ну.


            1. NeoCode
              26.06.2024 14:00

              Компилятор - штука детерминориванная. Или вы хотите сказать что встречая UB компилятор каждый раз обращается к генератору случайных чисел чтобы решить какой код генерировать? От младших битов к старшим или наоборот размещать битовые поля?


              1. mayorovp
                26.06.2024 14:00
                +1

                Помимо разных компиляторов существуют разные версии, разные опции и разные условия.


                1. NeoCode
                  26.06.2024 14:00

                  Подозреваю, что если кто нибудь в MSVC или GCC внезапно поменяет порядок битов в битовых полях, это будет проблема покруче чем проблема 2038 года".

                  Впрочем, то что комитет до сих пор этот порядок не стандартизировал, а вместо этого заставляет людей писать тонны бессмысленного бойлерплейтного кода, не делает им чести. Возможно существуют какие-то экзотические компиляторы, где порядок битов другой, и поэтому не хотят стандартизировать? Для этого в том же стандарте прописать некое требование к разработчикам компиляторов, чтобы они выпускали документы, в которых четко прописывали, как в их компиляторах разрешаются все эти "UB". Но это уже по сути бюрократия, мне не особо интересная:)


                  1. mayorovp
                    26.06.2024 14:00

                    Обязательный выпуск таких документов полностью противоречит термину "undefined behavior".

                    То, о чём вы пишете, называется "implementation-defined behavior", и такое в стандарте тоже есть.


              1. eao197
                26.06.2024 14:00
                +1

                Я хочу сказать, что в интернете уже столько развенчаний мифов о UB с наглядными примерами того, как UB стреляет по ногам, что нужно быть очень высокого мнения о себе чтобы пускаться в рассуждения о детерминизме компилятора.


    1. AnimeSlave Автор
      26.06.2024 14:00
      +2

      Сначала покритикую, а потом отвечу по существу.

      В какую же хрень превратился современный С++ :(

      С подключением! Простите за резкость, но вы явно не программист на C++. Вы из какого языка пришли в статью о C++? Из C? (Это риторический вопрос)
      Такой ситаксис появился ещё в 2011 году.

      не remove, а инверсия

      Что, простите? Вы не знаете как XOR работает?

      Ну а вообще есть битовые поля.

      Они от пачки булевых не отличаются. Использовать union при этом - это намеренно тащить UB в код. Спасибо. Не надо Не в каждой компании code review пройдёт


      1. mayorovp
        26.06.2024 14:00

        Что, простите? Вы не знаете как XOR работает?

        В том-то и дело, что знаем. К слову, та же самая операция XOR могла бы и в методе add применяться, но там вы "почему-то" использовали OR.


      1. NeoCode
        26.06.2024 14:00

        С подключением! Простите за резкость, но вы явно не программист на C++. Вы из какого языка пришли в статью о C++? Из C? (Это риторический вопрос)Такой ситаксис появился ещё в 2011 году.

        Что, простите? Вы не знаете как XOR работает?

        Как удачно совпало:) В вашем коде полно шаблонов (да еще и вариадических), constexpr'ов, static_cast'ов и прочего, все написано в лучших традициях "современного С++". Но все эти навороты вам не помогли - вы допустили ошибку, применив XOR, который инвертирует значение на противоположное вместо того, чтобы его обнулять, что ожидается по смыслу от функции с именем remove().


        1. AnimeSlave Автор
          26.06.2024 14:00

          Пройдите в википедию (хотя бы) для начала, и посмотрите, как работает XOR, чтобы не писать чуши.


          1. NeoCode
            26.06.2024 14:00
            +2

            Я прекрасно знаю как xor работает, постоянно его использую:)

            X Y X^Y
            0 0  0
            0 1  1
            1 0  1
            1 1  0

            Таким образом, если вы случайно вызовете метод remove() для некоторого бита два раза (в общем случае четное число раз), то результат будет такой же, как если бы вы его вообще не вызывали. Что принципиально отличается от add(), который можно вызывать какое угодно число раз - бит всегда будет устанавливаться в единицу. Также работа remove() будет зависеть от текущего состояния бита, а работа add() - нет.


            1. AnimeSlave Автор
              26.06.2024 14:00

              Если вы знаете как он работает, то почему вы его «инверсией» называете? Когда он «инверсией» в полном смысле не является. То, что где-то его называют инвертирование по маске, не делает его инверсией. А остальное я не буду комментировать. Напридумывали сами себе проблем, и пытаетесь меня ими попрекать. Ваши позицию я понял


              1. mayorovp
                26.06.2024 14:00

                Потому что, если зафиксировать второй операнд, таблица истинности превращается в вот такую:

                X 1 X^1
                0 1  1
                1 1  0
                

                Что это если не инверсия?

                Более того, в той же алгебре Жегалкина именно так инверсия и выражается.


                1. AnimeSlave Автор
                  26.06.2024 14:00

                  Что это если не инверсия?

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


                  1. mayorovp
                    26.06.2024 14:00

                    Вы можете хоть сто раз назвать всё чушью, но поведение вашего метода remove от этого соответствовать его названию не станет.


                    1. AnimeSlave Автор
                      26.06.2024 14:00

                      Давайте зафиксируем. Я знаю и принимаю факт, что идиоматический x&~y - это удаление. Но моя логика в том, что здесь уже две операции, против одной в моём варианте. Удалит ли XOR Условие, если это необходимо, после проверки на наличие? Удалит. XOR вернёт разницу. Был ли мой выбор оператора верным? Зависит от стороны спора. Дальше нет смысла дискутировать. Надеюсь читатель статьи прочитает комментарии, и делает свой выбор оператора для удаления


                      1. mayorovp
                        26.06.2024 14:00

                        Что-то вы странно операции считаете, у вас там условный оператор стоит, который сам по себе дороже побитовой инверсии.


                      1. AnimeSlave Автор
                        26.06.2024 14:00

                        Условный оператор там стоит для проверки. Об этом написано в статье. Мне она была нужна. Потому я её там и написал


                      1. mayorovp
                        26.06.2024 14:00

                        И снова эту проверку можно и без него сделать.


                      1. naviUivan
                        26.06.2024 14:00

                        Условный оператор там стоит для проверки. Об этом написано в статье. Мне она была нужна.

                        А не потому ли условный оператор с вызовом has() понадобился, что remove() не делает то, что в ее названии указано? А делает ту самую "инверсию" x^y вместо логичного и правильного x&~y.

                        Да и странно считать количество логических операций при этом используя условный оператор и вызов метода. Ясно, что там будет инлайн, да всякие оптимизации компилятора, но вы об этом не говорите.

                        Да и откуда взялись вообще ваши рассказы в комментариях про производительность? В тексте статьи об этом условии ни слова. И нужно ли вообще беспокоиться о производительности и колличестве логических опреторов в описанной вами задаче? Поэтому повторюсь - чем вас std::bitset не устраивает?


              1. naviUivan
                26.06.2024 14:00

                У вас метод reach(), который вызывает remove(), работает правильно только потому, что перед вызовом remove() вы делаете проверку has() на нулевое значение проверяемого бита и не вызываете remove() в этом случае если 0. А в случае когда бит равен 1, remove() его просто инвертирует в 0, как вам и пытаются тут показать, что XOR в remove() неуместен, выпполняет инверсию, и делает не то, что вы ожидаете, хоть и все работает в итоге правильно, но до тех пор, пока кто-то не удалил проверку на 0.


        1. AnimeSlave Автор
          26.06.2024 14:00

          Пройдите в википедию (хотя бы) для начала, и посмотрите, как работает XOR, чтобы не писать чуши.


  1. mayorovp
    26.06.2024 14:00
    +1

    Что-то инверсные условия выглядят каким-то переусложнением. Как и названия методов reached, lose, isReached - это ж головоломка какая-то. Да и разделение методов на публичные и приватные тут нафиг не нужно...

    Можно же проще:

    public:
        void set(EnumType flags) noexcept
        {
            value |= static_cast<EnumUnderlyingType>(flags);
        }
    
        void reset(EnumType set) noexcept
        {
            value &= ~static_cast<EnumUnderlyingType>(condition);
        }
    
        bool has(EnumType flags) const noexcept
        {
            return (value & static_cast<EnumUnderlyingType>(flags)) == static_cast<EnumUnderlyingType>(flags);
        }
    
        bool has_any(EnumType flags) const noexcept
        {
            return (value & static_cast<EnumUnderlyingType>(flags));
        }
    


    1. AnimeSlave Автор
      26.06.2024 14:00

      Что-то я изначально хотел длинный комментарий, разбирая ваш. А сейчас посидел и принял, что пусть так. Для вас это и переусложнение, и лишние приватные методы. К решению задачи можно подойти разными путями. Я подошёл таким, который описал в статье. А ваш комментарий - придирки по пустякам


      1. eao197
        26.06.2024 14:00
        +2

        А ваш комментарий - придирки по пустякам

        Тогда как со стороны он выглядит конструктивной критикой. И решение ваше простым не является, и по поводу именования методов полностью согласен с предыдущим оратором.

        ЗЫ. С собственными решениями всегда так. По началу они нравятся, хочется поделиться с народом удовольствием о того, что удалось придумать, сделать и заставить работать. Однако, не редко, годика эдак через три-четыре-пять, сам уже бываешь не рад и осознаешь, что нужно было делать проще.


        1. AnimeSlave Автор
          26.06.2024 14:00

          Тогда как со стороны он выглядит конструктивной критикой

          Я на самом деле принимаю эту критику. Только есть одна проблема. Я изначально в представленном коде написал комментарий о том, что приватные методы лишь для наглядности. Если то, кто скопирует код, и переделает это под свои критерии, то я не буду этому препятствовать. Он сам у себя дома хозяин. С моей стороны я постарался сделать наглядный пример. Но уже у второго комментатора придирка именно к части битовых операций. И оба отбрасывают возможность проверки. Опять же пусть так. Им не нужно. Но нужно было мне. И именно так и написан мной код

          upd.
          И придирка к наименованию. Что значит set? Оно снимает или устанавливает? А reset?

          isActive.set(Condition::INITIALIZED);
          isActive.reset(Condition::INITIALIZED);
          

          Особенно в свете вашего же тезиса о годах спустя


          1. mayorovp
            26.06.2024 14:00
            +1

            set устанавливает, reset сбрасывает.

            Это повсеместная терминология, Навскидку: RS-триггеры, объект Event в WinAPI, тот же bitset из стандартной библиотеки С++ - все используют set и reset именно так.


            1. AnimeSlave Автор
              26.06.2024 14:00

              Семантика слова reset подразумевает установку первоначального значения. А какое первоначальное значение у бита в переменной представленного мой контейнера? С учётом моего примера инициализации, то бит должен быть 1. Но ваша функция ставит его в 0. Я в этом не вижу очевидного поведения


              1. eao197
                26.06.2024 14:00

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

                Как бы reset означает сброс того, что было установлено раньше. Будет ли результат reset-а начальным значением или нет -- это уже от предметной области зависит.


                1. AnimeSlave Автор
                  26.06.2024 14:00

                  Как бы reset означает сброс того, что было установлено раньше.

                  Это никак не меняет понимание


              1. mayorovp
                26.06.2024 14:00

                Об этом я уже говорил, нет никакого смысла в хранении инвертированных флагов. Начальное значение - ноль.


          1. eao197
            26.06.2024 14:00
            +1

            Особенно в свете вашего же тезиса о годах спустя

            Вновь поддержу предыдущего оратора. set устанавливает, reset сбрасывает.
            Т.е. если кто-то вызывал:

            bits_collection.set(Condition::INITIALIZED);
            

            то я ожидаю, что условная функция проверки is_set вернет true:

            bits_collection.set(Condition::INITIALIZED);
            assert(bits_collection.is_set(Condition::INITIALIZED);
            

            Тогда как reset ведет к тому, что is_set вернет false:

            bits_collection.reset(Condition::INITIALIZED);
            assert(!bits_collection.is_set(Condition::INITIALIZED);
            

            Как признак set/unset будет хранится внутри -- это безразницы, хоть ноликом, хоть единичкой.

            PS. И да, не стоит в C++ давать идентификаторам названия в ИСКЛЮЧИТЕЛЬНО_ВЕРХНЕМ_РЕГИСТРЕ. Прямой путь к проблемам при интеграции с C-шным кодом.


            1. AnimeSlave Автор
              26.06.2024 14:00

              И да, не стоит в C++ давать идентификаторам названия в ИСКЛЮЧИТЕЛЬНО_ВЕРХНЕМ_РЕГИСТРЕ

              Это зависит от гайду по стилю коду принятого в компании, а не от ваших личных предпочтений. Названия перечислений и константные значения, на сколько я помню, принято указывать заглавными буквами. Очередная придирка по пустяку


              1. eao197
                26.06.2024 14:00

                а не от ваших личных предпочтений

                Если бы.

                Названия в CAPS_LOCK-е принято давать в C-шных макросах.
                Поэтому если вы у себя в проекте подключаете заголовок из чисто-сишной либы, то к вам могут прилететь макросы, о которых вы даже не подозреваете. И ваш код начнет вести себя непредсказуемо (хорошо хоть, что в основном во время компиляции).

                Я так когда-то наступил на грабли с названием LOG_ERR (или чем-то подобным).

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

                В Java :)

                Очередная придирка по пустяку

                Что-то мне подсказывает, что вы еще очень молоды.


                1. AnimeSlave Автор
                  26.06.2024 14:00

                  С константами в верхнем регистре я беру свои слова назад. Вот если бы вы мне ссылку на гайдлайн дали, то я бы не стал писать комментарий выше. Я не разработчик на Java, никогда им не был. И вообще Java не люблю. Потому не знаю про их стиль кода. Константы и перечисления с заглавными буквами, меня научили писать ещё очень давно. И я до сих пор так пишу, и в некоторых компаниях, где работал, принято так же. И сейчас я понимаю, что всё это время писал не правильно, если идти по гайдлайну по ссылке


  1. dr_alex
    26.06.2024 14:00

    Под впечатлением от решения подобной проблемы на стороне свифта, с помощью magic_enum был написан простой враппер над std::bitset, идея простая, в std::bitset поменять тип аргумента с std::size_t на enum class. Размер std::bitset можно вычислить с помощью magic_enum.

    Из плюсов тут то, что нет требования к значениям enum, что они должны быть степенями двойки, но есть требования этим значениям вообще ничего не присваивать, чтобы они определялись автоматически, иначе беда. Ждем рефлексию чтоб такое можно было делать более безопасно.

    Hidden text
    template <class Enum, std::size_t Size = magic_enum::enum_count<Enum>()>
    class OptionSet {
        std::bitset<Size> _bits;
    
      public:
        OptionSet() = default;
    
        OptionSet(Enum e) {
            set(e);
        }
    
        OptionSet(std::initializer_list<Enum> list) {
            for (auto e : list) {
                set(e);
            }
        }
    
        constexpr typename std::bitset<Size>::reference operator[](Enum e) {
            return _bits[std::to_underlying(e)];
        }
    
        constexpr bool operator[](Enum e) const {
            return _bits.test(std::to_underlying(e));
        }
    
        // Далее врапим все методы std::bitset
    };


    1. avbochagov
      26.06.2024 14:00

      вот только рефлексии и не хватает для простоты решения...

      почему все так носятся с рефлексией?


      1. eao197
        26.06.2024 14:00
        +1

        почему все так носятся с рефлексией?

        Потому что ее нет, вот и кажется, что это манна небесная. А как появится, как распробуем, так и начнем мечтать еще о чем-то недоступном в ожидании чуда :)