12

I'm looking to use a set of bit flags for my current issue. These flags are (nicely) defined as part of an enum, however I understand that when you OR two values from an enum the return type of the OR operation has type int.

What I'm currently looking for is a solution which will allow the users of the bit mask to remain type safe, as such I have created the following overload for operator |

enum ENUM
{
    ONE     = 0x01,
    TWO     = 0x02,
    THREE   = 0x04,
    FOUR    = 0x08,
    FIVE    = 0x10,
    SIX     = 0x20
};

ENUM operator | ( ENUM lhs, ENUM rhs )
{
    // Cast to int first otherwise we'll just end up recursing
    return static_cast< ENUM >( static_cast< int >( lhs ) | static_cast< int >( rhs ) );
}

void enumTest( ENUM v )
{
}

int main( int argc, char **argv )
{
    // Valid calls to enumTest
    enumTest( ONE | TWO | FIVE );
    enumTest( TWO | THREE | FOUR | FIVE );
    enumTest( ONE | TWO | THREE | FOUR | FIVE | SIX );

    return 0;
}

Does this overload really provide type safety? Does casting an int containing values not defined in the enum cause undefined behaviour? Are there any caveats to be aware of?

5
  • operator|(FIVE, SIX) == 0x30. What ENUM constant has value 0x30? Commented Oct 9, 2013 at 9:24
  • I wouldn't be doing any form of comparison on the resulting value, I'd simply be checking the flags. Commented Oct 9, 2013 at 9:26
  • 1
    Then the result of the OR should stay an int. Commented Oct 9, 2013 at 9:27
  • 1
    @Adam: Why? The result of the OR is a valid value for this enumeration. Commented Oct 9, 2013 at 9:45
  • BTW. Qt does what the OP is trying with a template QFlags<> you may want to take a look at that. qt-project.org/doc/qt-5.0/qtcore/qflags.html Commented Oct 9, 2013 at 11:30

5 Answers 5

7

Does this overload really provide type safety?

In this case, yes. The valid range of values for the enumeration goes at least up to (but not necessarily including) the next largest power of two after the largest named enumerator, in order to allow it to be used for bitmasks like this. So any bitwise operation on two values will give a value representable by this type.

Does casting an int containing values not defined in the enum cause undefined behaviour?

No, as long as the values are representable by the enumeration, which they are here.

Are there any caveats to be aware of?

If you were doing operations such as arithmetic, which could take the value out of range, then you'd get an implementation-defined result, but not undefined behavoiur.

Sign up to request clarification or add additional context in comments.

1 Comment

By "as long as the values are representable by the enumeration", do you mean that the values have to be representable by the enum's underlying type?
4

If you think about type safety, it is better to use std::bitset

enum BITS { A, B, C, D }; 
std::bitset<4> bset, bset1;
bset.set(A); bset.set(C);
bset1[B] = 1;
assert(bset[A] == bset[C]);
assert(bset[A] != bset[B]);
assert(bset1 != bset);

Comments

3

The values of your constants are not closed under OR. In other words, it's possible that the result of an OR of two ENUM constants will result in a value that is not an ENUM constant:

0x30 == FIVE | SIX;

The standard says that this is ok, an enumaration can have a value not equal to any of its enumarators (constants). Presumably it's to allow this type of usage.

In my opinion this is not type safe because if you were to look at the implementation of enumTest you have to be aware that the argument type is ENUM but it might have a value that's not an ENUM enumerator.

I think that if these are simply bit flags then do what the compiler wants you to: use an int for the combination of flags.

6 Comments

The cast is well-defined as long as the result is a valid enumeration value - as it is here. Valid values aren't just the named enumerators, but (at least) everything up to the next largest power of two - it's specified like that to allow exactly this kind of usage.
@MikeSeymour Do you have a reference for that? Everything I find says it's illegal.
C++11 7.2. Paragraph 7 is most relevant, since it defines the range of values.
@MikeSeymour the OP is asking specifically about C++03.
@MikeSeymour I assume you're referring to this sentence: "It is possible to define an enumeration that has values not defined by any of its enumerators."
|
2

With a simple enum such as yours:

enum ENUM
{
    ONE     = 0x01,
    TWO     = 0x02,
    ...
};

it is implementation-defined what's the underlying type (most likely int)1, but as long as you are going to use | (bitwise or) for creating masks, the result will never require a wider type than the largest value from this enum.


[1] "The underlying type of an enumeration is an integral type that can represent all the enumerator values defined in the enumeration. It is implementation-defined which integral type is used as the underlying type for an enumeration except that the underlying type shall not be larger than int unless the value of an enumerator cannot fit in an int or unsigned int."

Comments

1

This is my approach to bit flags:

template<typename E>
class Options {
      unsigned long values;
      constexpr Options(unsigned long v, int) : values{v} {}
   public:
      constexpr Options() : values(0) {}
      constexpr Options(unsigned n) : values{1UL << n} {}
      constexpr bool operator==(Options const& other) const {
         return (values & other.values) == other.values;
      }
      constexpr bool operator!=(Options const& other) const {
         return !operator==(other);
      }
      constexpr Options operator+(Options const& other) const {
         return {values | other.values, 0};
      }
      Options& operator+=(Options const& other) {
         values |= other.values;
         return *this;
      }
      Options& operator-=(Options const& other) {
         values &= ~other.values;
         return *this;
      }
};

#define DECLARE_OPTIONS(name) class name##__Tag; using name = Options
#define DEFINE_OPTION(name, option, index) constexpr name option(index)

You can use it like so:

DECLARE_OPTIONS(ENUM);
DEFINE_OPTIONS(ENUM, ONE, 0);
DEFINE_OPTIONS(ENUM, TWO, 1);
DEFINE_OPTIONS(ENUM, THREE, 2);
DEFINE_OPTIONS(ENUM, FOUR, 3);

Then ONE + TWO is still of type ENUM. And you can re-use the class to define multiple bit flag sets that are of different, incompatible types.

I personally don't like using | and & to set and test bits. It's the logical operation that needs to be done to set and test, but they don't express the meaning of the operation unless you think about bitwise operations. If you read out ONE | TWO you might think that you want either ONE or TWO, not necessarily both. This is why I prefer using + to add flags together and == to test if a flag is set.

See this blog post for more details on my suggested implementation.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.