Как запросить достоверность класса enum, используемого в качестве битовой маски, без приведения к базовому типу

У меня есть класс enum, который я использую для маскировки битов, например (в Unreal Engine, поэтому тип uint8)

enum class Level : uint8
{
    None = 0x0,
    Debug = 0x1,
    Info = 0x2,
    Warning = 0x4,
    ...
}

Я добавил встроенные операторы для |, & и ^, так что я могу использовать элемент в качестве фактической битовой маски (например, mask = Level::Warning | Level::Debug), например:

inline Level operator&(const Level& lhs, const Level& rhs)
{
    return (Level)((uint8)lhs & (uint8)rhs);
}

Теперь в какой-то момент я хочу запросить, установлен ли бит или нет. Однако это не работает без приведения к uint8:

if(mask & Level::Debug) //does not work, C2451
if((uint8)(mask & Level::Debug)) //does work

Есть ли способ заставить запрос работать без необходимости приведения его к базовому типу?

РЕДАКТИРОВАТЬ: цель состоит в том, чтобы код вызова был как можно короче (и читабельнее). Значение & в этом случае кажется таким же ясным, как использование вызова, такого как дополнительный метод any как предложено в аналогичном вопросе. Добавление кода == Level::Debug, конечно, тоже работает, но не сокращает используемый код. Изменение типа возвращаемого значения оператора работает, но в основном проблема смещается к присваиванию типа Level myLevel = Level::Debug | Level::Warning, поэтому я бы не стал улучшать свой код в целом.


person Tare    schedule 15.02.2021    source источник
comment
Изменить оператор, чтобы вернуть базовый тип?   -  person eerorika    schedule 15.02.2021
comment
Это было слишком очевидно для меня :) спасибо. Однако в этом случае что-то вроде Level myLevel = Level::Debug | Level::Warning больше не работает. Тогда мне придется решить, от кого из этих двух я больше склонен отказаться, я полагаю?   -  person Tare    schedule 15.02.2021
comment
Отвечает ли это на ваш вопрос? Как я могу использовать класс перечисления в логическом контекст?   -  person Simon Kraemer    schedule 15.02.2021
comment
Или вы можете использовать перечисление без области действия для неявных преобразований без изменения оператора.   -  person eerorika    schedule 15.02.2021
comment
OT: Я бы рекомендовал вам использовать static_cast, где это возможно: gcc.godbolt.org/z/qvhq9W   -  person Simon Kraemer    schedule 15.02.2021
comment
@eeorika Я думал об этом. Насколько я понял, следует избегать простых перечислений (хотя за неимением исследований я еще не читал, почему именно)   -  person Tare    schedule 15.02.2021
comment
Перечисления @Tare Plain могут привести к конфликтам имен и подвержены ошибкам из-за неявных преобразований.   -  person Simon Kraemer    schedule 15.02.2021
comment
@Tare: stackoverflow .com/questions/18335861/   -  person Simon Kraemer    schedule 15.02.2021
comment
@SimonKraemer Конфликты имен не являются проблемой, потому что их легко разрешить, определив перечисление в области.   -  person eerorika    schedule 15.02.2021
comment
Ответ PiotrNycz решит мою первоначальную проблему, но не будет иметь никаких проблем с простым перечислением (или не-проблем), если я правильно это понимаю. Таким образом, это, кажется, лучший способ сделать это для меня. @SimonKraemer относительно вашего логического контекста: моя проблема с приведением заключалась в том, чтобы код был коротким и лучше читаемым. Таким образом, технически это отвечает на вопрос, но не будет (значительным) улучшением.   -  person Tare    schedule 15.02.2021
comment
В данном случае вы можете сделать: (mask & Level::Debug) == Level::Debug.   -  person Jarod42    schedule 15.02.2021
comment
@eerorika Часто это легче сказать, чем сделать. Для нового кода это не проблема, если вы все сделаете правильно. Особенно, когда вы работаете с устаревшим кодом, должны сохранить совместимость с ABI/API или вам нужно использовать несколько старых библиотек C или C++, которые не беспокоились об этих вещах, это было и часто остается проблемой. Использование классов enum спасет вас и ваших пользователей в будущем.   -  person Simon Kraemer    schedule 15.02.2021
comment
@SimonKraemer Использование классов перечисления также не решает проблему с устаревшим кодом, в котором используются перечисления без области действия. Если у вас есть возможность определить новый класс перечисления, то у вас также есть возможность определить перечисление в области видимости.   -  person eerorika    schedule 15.02.2021
comment
@eerorika Я этого не делал и не согласен. Я просто сказал, что в прошлом были случаи, и определение области вокруг перечисления легко забывается, если у вас нет варианта использования прямо сейчас. Поэтому использование классов enum безопаснее для вашего будущего развития.   -  person Simon Kraemer    schedule 15.02.2021
comment
@eerorika Использование классов enum помогает писать более безопасный и удобный для сопровождения код. Аналогично использованию static_cast вместо приведения в стиле C. Если вы знаете, что делаете, это не имеет значения, но это может укусить вас за **, если вы не будете осторожны. Кроме того, даже если вы поместите свое перечисление в область: если эта область явно не ограничена вашим перечислением и только вашим перечислением, вы можете столкнуться с такими проблемами. Представьте перечисление в пространстве имен проекта или определенное внутри класса. Если вам нужно определить другое перечисление в том же классе в будущем, вам нужно либо провести рефакторинг (что может быть невозможно сохранить...   -  person Simon Kraemer    schedule 15.02.2021
comment
совместимость API @eeorika) или для создания странных значений перечисления. Есть причина, по которой в устаревшем коде объявление значения перечисления часто имеет префикс с типом перечисления. И да, коллизии имен — это лишь малая часть проблем, которые решаются с помощью enum-классов, на реальных живых примерах. Говоря о моем собственном опыте здесь.   -  person Simon Kraemer    schedule 15.02.2021
comment
@SimonKraemer There is a reason that in legacy code enum value declaration are often prefixed with the enum type. Причина в том, что в C не было и нет ни пространств имен, ни классов. В этом случае соглашение о префиксах имен позволяет избежать конфликтов имен. C++ решил проблему отсутствия пространств имен, введя пространства имен.   -  person eerorika    schedule 15.02.2021
comment
@SimonKraemer Конечно, классы enum также имеют удобную область действия, но это лишь малая часть того, что он делает, как вы сказали. Еще один важный аспект заключается в том, что он не допускает неявных преобразований в/из базового типа. Учитывая, что OP запрашивает способ избежать явных преобразований, перечисление кажется подходящим.   -  person eerorika    schedule 15.02.2021
comment
@eerorika Вы должны прочитать мои комментарии полностью, поскольку вы явно опускаете половину того, что я сказал. Я ответил на заявление ОП So far as I understood, plain enums should be avoided (although for lack of research I haven't yet read, why exactly). Я никогда не говорил, что этот вариант использования должен избегать простых перечислений. Я никогда не говорил, что простых перечислений следует избегать любой ценой. Я никогда не говорил, что конфликт имен был единственной проблемой с простыми перечислениями. Я дал общее объяснение, основанное на двух наиболее ярких примерах того, почему вообще следует избегать простых перечислений.   -  person Simon Kraemer    schedule 15.02.2021
comment
@SimonKraemer I answered to OP's statement И я расширил ваш ответ на это утверждение, чтобы объяснить, почему данная причина избегать перечислений не является причиной избегать перечислений в целом. Это причина избегать перечислений в глобальном (или иным образом переполненном) пространстве имен.   -  person eerorika    schedule 15.02.2021
comment
Давайте продолжим это обсуждение в чате.   -  person Simon Kraemer    schedule 15.02.2021


Ответы (1)


Вам нужен оператор bool, но оператор bool может быть определен только для типа класса. Итак, определите класс и поместите в него свое перечисление, например:

class Level
{

public:
    enum Value : std::uint8_t
    {
        None = 0x0,
        Debug = 0x1,
        Info = 0x2,
        Warning = 0x4,
        Error = 0x8
    };
    constexpr Level(Value value) noexcept : value(value) {}
    
    friend constexpr bool operator == (Level lhs, Level rhs)
    { return lhs.value == rhs.value; } 
    friend constexpr bool operator != (Level lhs, Level rhs)
    { return lhs.value != rhs.value; } 
    
    friend constexpr Level operator & (Value lhs, Value rhs)
    {
        return Value(static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs));
    }
    friend constexpr Level operator & (Level lhs, Level rhs)
    {
        return lhs.value & rhs.value;
    }
    constexpr explicit operator bool() const noexcept { return value != Value::None; }
    
private:
    Value value;
};


Рабочая демонстрация.

Используйте так:

int main() {
    Level l = Level::None;
    if (l & Level::Info) return -1;
}

person PiotrNycz    schedule 15.02.2021
comment
Я попробовал это, и сейчас у меня есть две проблемы: 1.) Я добавил оператор | практически так же, как вы предлагаете оператор &. Тем не менее, присвоение значения, подобного Level l = Level::Debug | Leve::Warning;, похоже, не работает (C2664 не может преобразовать аргумент 1 из «int» в «Level::Value». 2) Это не работает с переключателем (выражение переключателя C2450 типа «уровень» является недопустимым) . Есть ли способ обойти это? - person Tare; 15.02.2021
comment
Вы правы - я изменил ответ. Кажется, нужно 2 реализации для каждого оператора &,|,^,~ - для значения и уровня, как в моем ответе. - person PiotrNycz; 15.02.2021
comment
Это действительно решило проблему назначения. Однако проблема с переключением все еще остается. Я предполагал, что добавление оператора uint8 (похожего на оператор bool) поможет (и, возможно, так оно и было), но затем кажется, что он конфликтует с недавно добавленным оператором |. В любом случае задания затем жалуются на 3 похожих оператора - person Tare; 15.02.2021
comment
если вы хотите сделать что-то подобное switch(level) { case Level::None: ... }; добавьте специальный метод constexpr Value getValue() const { return value; } - switch(level.getValue()) { case Level::None: ... }; - person PiotrNycz; 15.02.2021
comment
И, возможно, вам понадобятся операторы == и != - я просто добавил их, чтобы ответить. В С++ 20 - просто выполните bool operator == (Level) const = default; - person PiotrNycz; 15.02.2021