-->

C++11 standard conformant bitmasks using enum clas

2019-03-08 14:25发布

问题:

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.

回答1:

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


回答2:

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


回答3:

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).



回答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!



回答5:

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).