1

I have an enum class I use for bit masking, like so (in Unreal Engine, therefore the uint8 type)

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

I have added inline operators for |, & and ^ such that I can use a member as the actual bitmask (e.g. mask = Level::Warning | Level::Debug), like so:

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

Now at some point I want to query, whether a bit is set or not. However, this does not seem to work without casting to uint8:

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

Is there a way to make the query work without having to cast it to the underlying type?

EDIT: The goal is to keep the calling code as short (and readable) as possible. The meaning of & in this case seems to be as clear as using a call such as an extra any method as suggested in a similar question. Adding the == Level::Debug code also works of course, but would not shorten the code used. Changing the return type of the operator works, but basically shifts the problem to assignment like Level myLevel = Level::Debug | Level::Warning, so I wouldn't overall improve my code imo.

Tare
  • 482
  • 1
  • 9
  • 25
  • Change the operator to return the underlying type? – eerorika Feb 15 '21 at 09:10
  • That was too obvious for me :) thanks. However, in that case something like `Level myLevel = Level::Debug | Level::Warning` does not work anymore. I will then have to decide which of these two I am more willing to drop, I guess? – Tare Feb 15 '21 at 09:15
  • 1
    Does this answer your question? [How can I use an enum class in a boolean context?](https://stackoverflow.com/questions/9874960/how-can-i-use-an-enum-class-in-a-boolean-context) – Simon Kraemer Feb 15 '21 at 09:20
  • Or you could use a non scoped enum for implicit conversions without changing the operator. – eerorika Feb 15 '21 at 09:20
  • OT: I would recommend you use `static_cast` where possible: https://gcc.godbolt.org/z/qvhq9W – Simon Kraemer Feb 15 '21 at 09:22
  • @eerorika I had thought about it. So far as I understood, plain enums should be avoided (although for lack of research I haven't yet read, why exactly) – Tare Feb 15 '21 at 09:34
  • 1
    @Tare Plain enums can lead to name collisions and are error prone because of the implicit conversions. – Simon Kraemer Feb 15 '21 at 09:40
  • @Tare: https://stackoverflow.com/questions/18335861/why-is-enum-class-preferred-over-plain-enum – Simon Kraemer Feb 15 '21 at 09:43
  • @SimonKraemer The name collisions are a non-problem because they are easy to resolve by defining the enum within a scope. – eerorika Feb 15 '21 at 09:45
  • The answer by PiotrNycz would solve my original issue but not have any of the plain enum problems (or non-problems) though, if I see this correctly. Thus it seems to be the best way to go about it to me. @SimonKraemer regarding your boolean context: My issue with the cast was keeping the code short and better readable. Thus it technically answers the question, but would not be a (major) improvement. – Tare Feb 15 '21 at 09:51
  • 1
    In current case, you might do: `(mask & Level::Debug) == Level::Debug`. – Jarod42 Feb 15 '21 at 09:53
  • @eerorika That's often easier said than done. For new code this is not really a problem if you "make it right". Especially when you are working with legacy code, must retain ABI/API compatibility or need to use multiple older C or C++ libraries that did not bother about these things this was and is often a problem. Using enum classes will save you and your users in the future. – Simon Kraemer Feb 15 '21 at 09:54
  • @SimonKraemer Using enum classes doesn't solve the problem with legacy code that uses unscoped enums either. If you have the option of defining a new enum class, then you also have the option of defining an enum within a scope. – eerorika Feb 15 '21 at 09:58
  • @eerorika I did not and I don't disagree. I just said that there were cases in the past and defining a scope around an enum is easily forgotten if you don't have the use case right now. So using enum classes is safer for your future development. – Simon Kraemer Feb 15 '21 at 10:09
  • @eerorika Using enum classes helps writing safer, better maintanable code. Similar to using `static_cast` over C-style-cast. If you know what you are doing it makes no difference but it might bite you in the a** if you are not careful. Also even if you put your enum in a scope: If that scope is not explicitly restricted to your enum and your enum only you may run into problems like this. Think about an enum in the project namespace or defined inside a class. If you need to define another enum in the same class in the future you need to either refactor (which might not be possible to keep... – Simon Kraemer Feb 15 '21 at 10:19
  • @eerorika API compatibility) or to make up strange enum values. There is a reason that in legacy code enum value declaration are often prefixed with the enum type. And yes, name collisions is only one small part of the problems that are solved with enum classes, it is one with real live examples. Speaking of my own experience here. – Simon Kraemer Feb 15 '21 at 10:21
  • @SimonKraemer `There is a reason that in legacy code enum value declaration are often prefixed with the enum type.` The reason is that C didn't and doesn't have namespaces nor classes. In that case the convention of prefixing names is the way to avoid name collisions. C++ solved the lack of namespaces by introducing namespaces. – eerorika Feb 15 '21 at 10:28
  • @SimonKraemer Sure, enum classes also have a scope which is convenient, but that is only a small part of what it does as you said. Another important aspect is that it doesn't allow implicit conversions to / from the underlying type. Given that OP is asking a way to avoid explicit conversions, enum seems like a good fit. – eerorika Feb 15 '21 at 10:28
  • @eerorika You should read my comments in full as you are clearly omitting half of what I said. I answered to OP's statement `So far as I understood, plain enums should be avoided (although for lack of research I haven't yet read, why exactly)`. Never did I say that this use-case should avoid plain enums. Never did I say that plain enums must be avoided at all cost. Never did I say that name collision was the only problem with plain enums. I was giving a common explanation based on the two most prominent examples on why to avoid plain enums in general. – Simon Kraemer Feb 15 '21 at 10:54
  • @SimonKraemer `I answered to OP's statement` And I expanded on your answer to that statement to explain why the given reason to avoid enums is not a reason to avoid enums in general. It is a reason to avoid enums in a global (or otherwise crowded) namespace – eerorika Feb 15 '21 at 11:06
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/228738/discussion-between-simon-kraemer-and-eerorika). – Simon Kraemer Feb 15 '21 at 11:57

1 Answers1

2

You need operator bool - but operator bool can only be defined for class type. So - define class - and put your enum within it, like this:

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;
};


Working demo.

Use like this:

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

PiotrNycz
  • 23,099
  • 7
  • 66
  • 112
  • I have tried this and there are two problems I have right now: 1.) I have added the `|` operator practically the same way you suggest the `&` operator. Still, assigning a value like `Level l = Level::Debug | Leve::Warning;` does not seem to work (C2664 cannot convert argument 1 from 'int' to 'Level::Value'. 2) This does not work with switch (C2450 switch expression of type ' Level' is illegal). Is ther any way around this? – Tare Feb 15 '21 at 10:46
  • You're right - I modified the response. It seems it is needed 2 implementations for each &,|,^,~ operators - for Value and Level as in my answer. – PiotrNycz Feb 15 '21 at 11:23
  • That did indeed solve the assignment problem. However the switch problem still remains. I had assumed that putting a `uint8` operator (similar to the `bool` operator) would do the trick (and it might have), but it then seems to clash with the newly added `|` operator. In any case the assignments then complain about "3 similar operators" – Tare Feb 15 '21 at 13:24
  • 1
    if you want to do sth like this `switch(level) { case Level::None: ... };` add special method `constexpr Value getValue() const { return value; }` - `switch(level.getValue()) { case Level::None: ... };` – PiotrNycz Feb 15 '21 at 13:47
  • And it is possible you'd need operator == and != - I just added them to answer . In c++20 - just do `bool operator == (Level) const = default;` – PiotrNycz Feb 15 '21 at 13:50