Are enums the canonical way to implement bit flags?
Asked Answered
B

3

14

Currently I'm using enums to represent a state in a little game experiment. I declare them like so:

namespace State {
  enum Value {
    MoveUp = 1 << 0, // 00001 == 1
    MoveDown = 1 << 1, // 00010 == 2
    MoveLeft = 1 << 2, // 00100 == 4
    MoveRight = 1 << 3, // 01000 == 8
    Still = 1 << 4, // 10000 == 16
    Jump = 1 << 5
  };
}

So that I can use them this way:

State::Value state = State::Value(0);
state = State::Value(state | State::MoveUp);
if (mState & State::MoveUp)
  movement.y -= mPlayerSpeed;

But I'm wondering if this is the right way to implement bit flags. Isn't there a special container for bit flags? I heard about std::bitset, is it what I should use? Do you know something more efficient?
Am I doing it right?


I forgot to point out I was overloading the basic operators of my enum:
inline State::Value operator|(State::Value a, State::Value b)
{ return static_cast<State::Value>(static_cast<int>(a) | static_cast<int>(b)); }

inline State::Value operator&(State::Value a, State::Value b)
{ return static_cast<State::Value>(static_cast<int>(a) & static_cast<int>(b)); }


inline State::Value& operator|=(State::Value& a, State::Value b)
{ return (State::Value&)((int&)a |= (int)b); }

I had to use a C-style cast for the |=, it didn't work with a static_cast - any idea why?

Binkley answered 16/6, 2014 at 20:11 Comment(9)
There's this, but I'm not sure if it's the "canonical" way to do things... https://mcmap.net/q/67293/-which-typesafe-enum-in-c-are-you-usingTenedos
Too heavy for me. I want the lightest method possible, using only pure C++ or the standard library if necessary. No Boost. My goal here is to minimize memory and CPU usage because I'm making a game, and a millisecond of delay would mean a huge drop in the number of fps.Rudie
I can understand "no Boost", but I can't understand the concern about memory or CPU usage at run time, since these are all compile-time constants, not run-time values...Tenedos
I don't use Boost, but what I see in your link is the use of Boost-specific functions, casts to special classes like Abort or Error... unnecessary processing.Rudie
I believe the only processing required there is if you want to get the string associated with the enum, which you shouldn't be doing very often in a game engine anyway. Otherwise the enum is public inside the class and it's just like any other enum...Tenedos
@Mehrdad: It looks like that does a straight enum with sequential values. Can it be used to specify a bit flag enum?Emma
@FredLarson: Whoops! You're right, for some reason I totally forgot about that when looking it up... my bad...Tenedos
I found an alternative technique using structs of bool - not technically bit flags, but can replace them sometimes. Link: https://mcmap.net/q/726104/-when-to-use-stl-bitsets-instead-of-separate-variablesRudie
See my answer to this other question for my suggestion on how to define bit flag sets.Armure
F
8

I believe that your approach is right (except several things):
1. You can explicitly specify underlying type to save memory;
2. You can not use unspecified enum values.

namespace State {
  enum Value : char {
    None      = 0,
    MoveUp    = 1 << 0, // 00001 == 1
    MoveDown  = 1 << 1, // 00010 == 2
    MoveLeft  = 1 << 2, // 00100 == 4
    MoveRight = 1 << 3, // 01000 == 8
    Still     = 1 << 4, // 10000 == 16
    Jump      = 1 << 5
  };
}

and:

State::Value state = State::Value::None;
state = State::Value(state | State::MoveUp);
if (mState & State::MoveUp) {
  movement.y -= mPlayerSpeed;
}

about overloading:

inline State::Value& operator|=(State::Value& a, State::Value b) {
    return a = static_cast<State::Value> (a | b);
}

and since you use C++11, you should use constexpr every were is possible:

inline constexpr State::Value operator|(State::Value a, State::Value b) {
    return a = static_cast<State::Value> (a | b);
}

inline constexpr State::Value operator&(State::Value a, State::Value b) {
    return a = static_cast<State::Value> (a & b);
}
Faulk answered 16/6, 2014 at 20:23 Comment(14)
Why mState &= ~State::MoveUp;? What exactly does it do? And why make my enum inherit from char? Regarding the |= operator: I'll edit my question, I have trouble using it.Rudie
The same as subtraction. I mean that after analyze of flag you need switch it off.Faulk
But I don't want to switch it off. I analyze it in several modules of my game. By the way, I finally managed to implement the |= operator by using a C-style cast in the overload. You didn't answer my question regarding the inheritance from char.Rudie
Obviously it doesn't work without operator overloading.Rudie
@filsmick: That syntax enum Value : char isn't really inheritance, it is the C++11 syntax to tell the compiler you want the integral type that the enum is based on to be a char. In previous versions of C++, you weren't allowed to specify that. In this case, it means your sizeof(Value) == sizeof(char)Pomfret
Thanks, I understand now. However using a char here should be done with cautious, since sizeof(char) is usually 1, which means you can only represent 8 values in the enum instead of 32 if you use an int. Correct me if I'm wrong.Rudie
@filsmick: sizeof(char) is 1 by definition, but that doesn't necessarily mean it's 8 bits. It could be more on some platforms.Emma
@FredLarson I knew someone would say that when I wrote my comment. That's why I said usually :)Rudie
@filsmick, I'd like demonstrate the approach how to manage memory. Obviously you should use more appropriate type int16_t, int32_t or int64_t.Faulk
@Faulk You're right about the _t types, one should not rely on the standard size. Incidentally, I did a quick search and saw it wasn't strictly forbidden to ask for upvotes, so... since I am right, would you mind upvoting the comment where I said something right?Rudie
@filsmick: "usually" is incorrect. sizeof(char) is ALWAYS 1, no matter how many bits it uses.Emma
@FredLarson Really? I didn't know that, I thought it returned the actual size on the platform used - thanks for pointing it out.Rudie
It does return the actual size of the smallest addressable unit on the platform used, which is the definition of 1 byte/char. If that's 10 bits, so be it.Counterwork
I suggest specifying a portable unsigned integer as the underlying type, e.g.: enum Value : uint32_t {. Choose a type big enough to hold your largest value. This requires C++11 or newer, and may avoid issues using signed integers with bitwise operations - some of which are undefined behavior. Since the specific values are added to the global namespace, you may want to name them like Value_None, Value_MoveUp, etc., where Value is replaced by something more meaningful. Scoped enums solve this, too, but then you don't inherit the bitwise operators from the unit32_t type.Mckamey
O
9

The STL contains std::bitset, which you can use for precisely such a case.

Here is just enough code to illustrate the concept:

#include <iostream>
#include <bitset>

class State{
public:
    //Observer
    std::string ToString() const { return state_.to_string();};
    //Getters
    bool MoveUp()    const{ return state_[0];}; 
    bool MoveDown()  const{ return state_[1];}; 
    bool MoveLeft()  const{ return state_[2];}; 
    bool MoveRight() const{ return state_[3];}; 
    bool Still()     const{ return state_[4];}; 
    bool Jump()      const{ return state_[5];}; 
    //Setters
    void MoveUp(bool on)    {state_[0] = on;}
    void MoveDown(bool on)  {state_[1] = on;}
    void MoveLeft(bool on)  {state_[2] = on;}
    void MoveRight(bool on) {state_[3] = on;}
    void Still(bool on)     {state_[4] = on;}
    void Jump(bool on)      {state_[5] = on;}
private:
    std::bitset<6> state_;
};


int main() {
    State s;
    auto report = [&s](std::string const& msg){
        std::cout<<msg<<" "<<s.ToString()<<std::endl;
    };
    report("initial value");
    s.MoveUp(true);
    report("move up set");
    s.MoveDown(true);
    report("move down set");
    s.MoveLeft(true);
    report("move left set");
    s.MoveRight(true);
    report("move right set");
    s.Still(true);
    report("still set");
    s.Jump(true);
    report("jump set");
    return 0;
}

Here's it working: http://ideone.com/XLsj4f

The interesting thing about this is that you get std::hash support for free, which is typically one of the things you would need when using state inside various data structures.

EDIT: There is one limitation to std::bitset and that is the fact that you need to know the maximum number of bits in your bitset at compile time. However, that is the same case with enums anyway.

However, if you don't know the size of your bitset at compile time, you can use boost::dynamic_bitset, which according to this paper (see page 5) is actually really fast. Finally, according to Herb Sutter, std::bitset was designed to be used in cases you would normally want to use std::vector.

That said, there really is no substitute for real world tests. So if you really want to know, profile. That will give you performance numbers for a context that you care about.

I should also mention that std::bitset has an advantage that enum does not - there is no upper limit on the number of bits you can use. So std::bitset<1000> is perfectly valid.

Ornelas answered 16/6, 2014 at 21:11 Comment(1)
Thanks for explaining how to use bitsets for bit flags! I'd be curious to see a benchmark of this vs. bit enums.Rudie
F
8

I believe that your approach is right (except several things):
1. You can explicitly specify underlying type to save memory;
2. You can not use unspecified enum values.

namespace State {
  enum Value : char {
    None      = 0,
    MoveUp    = 1 << 0, // 00001 == 1
    MoveDown  = 1 << 1, // 00010 == 2
    MoveLeft  = 1 << 2, // 00100 == 4
    MoveRight = 1 << 3, // 01000 == 8
    Still     = 1 << 4, // 10000 == 16
    Jump      = 1 << 5
  };
}

and:

State::Value state = State::Value::None;
state = State::Value(state | State::MoveUp);
if (mState & State::MoveUp) {
  movement.y -= mPlayerSpeed;
}

about overloading:

inline State::Value& operator|=(State::Value& a, State::Value b) {
    return a = static_cast<State::Value> (a | b);
}

and since you use C++11, you should use constexpr every were is possible:

inline constexpr State::Value operator|(State::Value a, State::Value b) {
    return a = static_cast<State::Value> (a | b);
}

inline constexpr State::Value operator&(State::Value a, State::Value b) {
    return a = static_cast<State::Value> (a & b);
}
Faulk answered 16/6, 2014 at 20:23 Comment(14)
Why mState &= ~State::MoveUp;? What exactly does it do? And why make my enum inherit from char? Regarding the |= operator: I'll edit my question, I have trouble using it.Rudie
The same as subtraction. I mean that after analyze of flag you need switch it off.Faulk
But I don't want to switch it off. I analyze it in several modules of my game. By the way, I finally managed to implement the |= operator by using a C-style cast in the overload. You didn't answer my question regarding the inheritance from char.Rudie
Obviously it doesn't work without operator overloading.Rudie
@filsmick: That syntax enum Value : char isn't really inheritance, it is the C++11 syntax to tell the compiler you want the integral type that the enum is based on to be a char. In previous versions of C++, you weren't allowed to specify that. In this case, it means your sizeof(Value) == sizeof(char)Pomfret
Thanks, I understand now. However using a char here should be done with cautious, since sizeof(char) is usually 1, which means you can only represent 8 values in the enum instead of 32 if you use an int. Correct me if I'm wrong.Rudie
@filsmick: sizeof(char) is 1 by definition, but that doesn't necessarily mean it's 8 bits. It could be more on some platforms.Emma
@FredLarson I knew someone would say that when I wrote my comment. That's why I said usually :)Rudie
@filsmick, I'd like demonstrate the approach how to manage memory. Obviously you should use more appropriate type int16_t, int32_t or int64_t.Faulk
@Faulk You're right about the _t types, one should not rely on the standard size. Incidentally, I did a quick search and saw it wasn't strictly forbidden to ask for upvotes, so... since I am right, would you mind upvoting the comment where I said something right?Rudie
@filsmick: "usually" is incorrect. sizeof(char) is ALWAYS 1, no matter how many bits it uses.Emma
@FredLarson Really? I didn't know that, I thought it returned the actual size on the platform used - thanks for pointing it out.Rudie
It does return the actual size of the smallest addressable unit on the platform used, which is the definition of 1 byte/char. If that's 10 bits, so be it.Counterwork
I suggest specifying a portable unsigned integer as the underlying type, e.g.: enum Value : uint32_t {. Choose a type big enough to hold your largest value. This requires C++11 or newer, and may avoid issues using signed integers with bitwise operations - some of which are undefined behavior. Since the specific values are added to the global namespace, you may want to name them like Value_None, Value_MoveUp, etc., where Value is replaced by something more meaningful. Scoped enums solve this, too, but then you don't inherit the bitwise operators from the unit32_t type.Mckamey
T
5

To be honest I don't think there is a consistent pattern for them.

Just look at std::ios_base::openmode and std::regex_constants::syntax_option_type as two completely different ways of structuring it in the standard library -- one using a struct, the other using an entire namespace. Both are enums all right, but structured differently.
Check your standard library implementation to see the details of how the above two are implemented.

Tenedos answered 16/6, 2014 at 20:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.