39

Can you implement standard conformant (as described in 17.5.2.1.3 of the n3242 draft) type safe bitmasks using enum class? The way I read it, a type T is a bitmask if it supports the |,&,^,~,|=,&= and ^= operators and further you can do if(l&r) where l and r are of type T. Missing from the list are the operator != and == and to allow sorting one probably also wants to overload <.

Getting the operators to works is just annoying boilerplate code but I do not see how to do if(l&r). At least the following does not compile with GCC (besides being extremely dangerous as it allows an erroneous implicit conversion to int):

enum class Foo{
    operator bool(){
        return (unsigned)*this;
    }
};

EDIT: I now know for certain that enum classes can not have members. The actual question how to do if(l&r) remains though.

B.S.
  • 1,435
  • 2
  • 12
  • 18
  • 4
    Can enum classes even *have* member functions? – Kerrek SB Aug 21 '12 at 17:19
  • @Kerrek SB I suspect that no, but then I do not know how to do if(l&r). – B.S. Aug 21 '12 at 17:25
  • 2
    I do not see any requirement that a bitmask type be implicitly convertible to `bool`; why must `if (l&r)` be well-formed? – James McNellis Aug 21 '12 at 17:26
  • @James McNellis How should you then test if a specific flag is set? Are you required to always write != 0 and == 0? EDIT: Also std::bitset supports conversion to boolean and is explicitly allowed. – B.S. Aug 21 '12 at 17:27
  • Yes. (Admittedly, this would be inconvenient, but I don't see that implicit conversion to `bool` is _required_ by the specification.) – James McNellis Aug 21 '12 at 17:29
  • @James McNellis Where is an operator ==0 or !=0 required according to the specification? Note that 0 is of type int which differs from the enum class type and therefore the == and != operators are not generated implicitly. The only really conforming way I see is (a&b)==b to test for a flag b. – B.S. Aug 21 '12 at 17:32
  • 1
    n3337 (17.5.2.1.3/4) says: "The value `Y` is set in the object `X` if the expression `X & Y` is nonzero." That looks to me like `==0` or `!=0` is required. – Robᵩ Aug 21 '12 at 17:34
  • 5
    Have you considered... not bothering? Personally, I've always seen type-safe enums as a way of providing reasonable assurance that an enumeration value will always be one of its enumerator types. If you can do bitwise operations on them, then this assumption is wrong (proven by the amount of `static_cast`s you'll need to make it compile). The way I see it, if you want a bitfield, you should use an `enum`, not an `enum class`. – Nicol Bolas Aug 21 '12 at 17:38
  • Have you tried making the operator as a free function? – Daniel Aug 21 '12 at 18:13
  • 3
    I have written such operators as templates, and I use a `has_flags` function to test if a value has all of a certain set of flags set. I find it is a lot better than `&`, because it can check multiple flags without potential for mistakes (writing `(l&r)` instead of `(l&r)==r`); and it is a lot more readable. – R. Martinho Fernandes Aug 21 '12 at 18:30
  • @Dani How do you make a conversion operator a free function? In C++98 this was not possible and I suspect that it still isn't. – B.S. Aug 21 '12 at 18:42
  • Avoid half your `static_cast`s and all your `std::underlying_type`s: If `e` is an expression with enum type, then `+e` is a number with the same value. – aschepler Aug 23 '12 at 01:13
  • 3
    @aschepler, who are you talking to? The question is about scoped enums, so that isn't true. – Jonathan Wakely Aug 23 '12 at 01:17

5 Answers5

39

I think you can... You'll have to add operators for bitmasky things. I didn't do it here but you could easily overload any relational operator.

  /**
   *
   */
  // NOTE: I changed to a more descriptive and consistent name
  //       This needs to be a real bitmask type.
  enum class file_permissions : int
  {
    no_perms        = 0,

    owner_read      =  0400,
    owner_write     =  0200,
    owner_exe       =  0100,
    owner_all       =  0700,

    group_read      =   040,
    group_write     =   020,
    group_exe       =   010,
    group_all       =   070,

    others_read     =    04,
    others_write    =    02,
    others_exe      =    01,
    others_all      =    07,

    all_all     = owner_all | group_all | others_all, // 0777

    set_uid_on_exe  = 04000,
    set_gid_on_exe  = 02000,
    sticky_bit      = 01000,

    perms_mask      = all_all | set_uid_on_exe | set_gid_on_exe | sticky_bit, // 07777

    perms_not_known = 0xffff,

    add_perms       = 0x1000,
    remove_perms    = 0x2000,
    symlink_perms   = 0x4000
  };

  inline constexpr file_permissions
  operator&(file_permissions x, file_permissions y)
  {
    return static_cast<file_permissions>
      (static_cast<int>(x) & static_cast<int>(y));
  }

  inline constexpr file_permissions
  operator|(file_permissions x, file_permissions y)
  {
    return static_cast<file_permissions>
      (static_cast<int>(x) | static_cast<int>(y));
  }

  inline constexpr file_permissions
  operator^(file_permissions x, file_permissions y)
  {
    return static_cast<file_permissions>
      (static_cast<int>(x) ^ static_cast<int>(y));
  }

  inline constexpr file_permissions
  operator~(file_permissions x)
  {
    return static_cast<file_permissions>(~static_cast<int>(x));
  }

  inline file_permissions &
  operator&=(file_permissions & x, file_permissions y)
  {
    x = x & y;
    return x;
  }

  inline file_permissions &
  operator|=(file_permissions & x, file_permissions y)
  {
    x = x | y;
    return x;
  }

  inline file_permissions &
  operator^=(file_permissions & x, file_permissions y)
  {
    x = x ^ y;
    return x;
  }
Pharap
  • 3,826
  • 5
  • 37
  • 51
emsr
  • 15,539
  • 6
  • 49
  • 62
  • I really like the simle an clear look of the code. Thanks for your answer. – Meister Schnitzel Feb 09 '15 at 06:34
  • This approach is used to implement `std::launch` enumeration in *gcc* implementation of standard library. std::launch is enum class but still supports bitwise logic operations on it. – anton_rh Jun 01 '16 at 03:12
  • 11
    Never use `__` in your identifiers - the Standard reserves them for implementations (for custom keywords, internal macros in standard library headers etc) – Pavel Minaev Jul 20 '16 at 23:17
  • 4
    @PavelMinaev Yes, true. This cose actually came from an attempt to implement for libstdc++. I should have cleaned it. – emsr Nov 30 '16 at 17:31
  • Since C++14 you can add `constexpr` to all overloads. – Xeverous Mar 28 '21 at 20:02
12

I'm not entirely sure what your acceptance criteria are, but you can just make operator & return a wrapper class with appropriate conversions and an explicit operator bool:

#include <type_traits>

template<typename T> using Underlying = typename std::underlying_type<T>::type;
template<typename T> constexpr Underlying<T>
underlying(T t) { return Underlying<T>(t); }

template<typename T> struct TruthValue {
    T t;
    constexpr TruthValue(T t): t(t) { }
    constexpr operator T() const { return t; }
    constexpr explicit operator bool() const { return underlying(t); }
};

enum class Color { RED = 0xff0000, GREEN = 0x00ff00, BLUE = 0x0000ff };
constexpr TruthValue<Color>
operator&(Color l, Color r) { return Color(underlying(l) & underlying(r)); }

All your other operators can continue to return Color, of course:

constexpr Color
operator|(Color l, Color r) { return Color(underlying(l) | underlying(r)); }
constexpr Color operator~(Color c) { return Color(~underlying(c)); }

int main() {
    constexpr Color YELLOW = Color::RED | Color::GREEN;
    constexpr Color WHITE = Color::RED | Color::GREEN | Color::BLUE;
    static_assert(YELLOW == (WHITE & ~Color::BLUE), "color subtraction");
    return (YELLOW & Color::BLUE) ? 1 : 0;
}
ecatmur
  • 152,476
  • 27
  • 293
  • 366
4

Scoped enumerations (those created with either enum class or enum struct) are not classes. They cannot have member functions, they just provide enclosed enumerators (not visible at namespace level).

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
4

Missing from the list are the operator != and ==

Those operators are already supported by enumeration types, integer types and std::bitset so there's no need to overload them.

and to allow sorting one probably also wants to overload <.

Why do you want to sort bitmasks? Is (a|b) greater than (a|c)? Is std::ios::in less than std::ios::app? Does it matter? The relational operators are always defined for enumeration types and integer types anyway.

To answer the main question, you would implement & as an overloaded non-member function:

Foo operator&(Foo l, Foo r)
{
    typedef std::underlying_type<Foo>::type ut;
    return static_cast<Foo>(static_cast<ut>(l) & static_cast<ut>(r));
}

I believe all the required operations for bitmask types could be defined for scoped enums, but not the requirements such as

Ci & Cj is nonzero and Ci & Cj iszero

and:

The value Y is set in the object X is the expression X & Y is nonzero.

Since scoped enums don't support impicit conversion to integer types, you cannot reliably test if it's nonzero or not. You would need to write if ((X&Y) != bitmask{}) and I don't think that's the intention of the committee.

(I initially thought they could be used to define bitmask types, then remembered I'd tried to implement one using scoped enums and encountered the problem with testing for zero/nonzero.)

Edit: I've just remembered that std::launch is a scoped enum type and a bitmask type ... so apparently scoped enums can be bitmask types!

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
3

A short example of enum-flags below.

#indlude "enum_flags.h"

ENUM_FLAGS(foo_t)
enum class foo_t
    {
     none           = 0x00
    ,a              = 0x01
    ,b              = 0x02
    };

ENUM_FLAGS(foo2_t)
enum class foo2_t
    {
     none           = 0x00
    ,d              = 0x01
    ,e              = 0x02
    };  

int _tmain(int argc, _TCHAR* argv[])
    {
    if(flags(foo_t::a & foo_t::b)) {};
    // if(flags(foo2_t::d & foo_t::b)) {};  // Type safety test - won't compile if uncomment
    };

ENUM_FLAGS(T) is a macro, defined in enum_flags.h (less then 100 lines, free to use with no restrictions).

Yuri Yaryshev
  • 991
  • 6
  • 24
  • Life saver from re-cording boilerplate each time! – Warpspace Feb 24 '16 at 06:42
  • Looks like you have a bug there. Line #68 should say enum class T: INT_T; instead of enum class T; Otherwise C3433: 'eee': all declarations of an enumeration must have the same underlying type – Soonts Aug 07 '16 at 04:26
  • This is super-useful. Question: why are you using `intptr_t` over e.g. `uint32_t`? – j b Apr 30 '19 at 12:24
  • @j intptr_t matches machine register size. It is 64 bit for modern machines and 32 for older ones and it can even be 16 bit if you're using it on some exotic platform, so intptr_t seems to be the best candidate for default type for enum. But you might have noticed that there's also a macro which gets type as argument – Yuri Yaryshev Apr 30 '19 at 14:24