【问题标题】:C++11 standard conformant bitmasks using enum class使用枚举类的 C++11 标准符合位掩码
【发布时间】:2012-08-17 02:07:35
【问题描述】:

您能否使用枚举类实现符合标准(如 n3242 草案的 17.5.2.1.3 中所述)类型的安全位掩码?我读它的方式,如果类型 T 支持 |,&,^,~,|=,&= 和 ^= 运算符,则它是位掩码,并且您可以进一步执行 if(l&r) 其中 l 和 r 属于 T 类型. 列表中缺少运算符 != 和 == 并允许排序一个可能还想重载 <.>

让操作员工作只是烦人的样板代码,但我不知道该怎么做 if(l&r)。至少以下内容不能使用 GCC 编译(除了极其危险,因为它允许错误地隐式转换为 int):

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

编辑:我现在确定枚举类不能有成员。但是,如何做 if(l&r) 的实际问题仍然存在。

【问题讨论】:

  • 枚举类甚至可以成员函数吗?
  • 我没有看到任何要求位掩码类型可以隐式转换为bool;为什么if (l&amp;r) 必须格式正确?
  • 你有没有考虑过……不打扰?就个人而言,我一直将类型安全枚举视为一种合理保证枚举值始终是其枚举类型之一的方法。如果你可以对它们进行按位运算,那么这个假设是错误的(通过你需要编译的static_casts 的数量来证明)。在我看来,如果你想要一个位域,你应该使用enum,而不是enum class
  • 我已经编写了诸如模板之类的运算符,并且我使用has_flags 函数来测试一个值是否设置了所有特定的标志集。我发现它比&amp; 好很多,因为它可以检查多个标志而不会出错(写(l&amp;r) 而不是(l&amp;r)==r);而且可读性更强。
  • @aschepler,你在和谁说话?问题是关于范围枚举,所以这不是真的。

标签: c++ c++11 bit-fields bitmask enum-class


【解决方案1】:

我觉得你可以... 您必须为位掩码添加运算符。我没有在这里做,但您可以轻松地重载任何关系运算符。

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

【讨论】:

  • 我真的很喜欢代码的简洁明了。谢谢你的回答。
  • 该方法用于在标准库的gcc实现中实现std::launch枚举。 std::launch 是枚举类,但仍然支持按位逻辑操作。
  • 永远不要在你的标识符中使用__——标准将它们保留用于实现(用于自定义关键字、标准库头文件中的内部宏等)
  • @PavelMinaev 是的,是的。这个问题实际上来自为 libstdc++ 实现 的尝试。我应该把它清理干净。
  • 从 C++14 开始,您可以将 constexpr 添加到所有重载中。
【解决方案2】:

我不完全确定您的接受标准是什么,但您可以让 operator &amp; 返回一个带有适当转换的包装类和一个 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)); }

当然,所有其他操作员都可以继续返回Color

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】:

    范围枚举(使用enum classenum struct 创建的枚举)不是类。它们不能有成员函数,它们只是提供封闭的枚举器(在命名空间级别不可见)。

    【讨论】:

    • 它们还禁止无范围枚举允许的隐式转换。
    【解决方案4】:

    列表中缺少运算符 != 和 ==

    枚举类型、整数类型和std::bitset 已经支持这些运算符,因此无需重载它们。

    并允许排序一个可能还想重载<.>

    为什么要对位掩码进行排序? (a|b) 是否大于 (a|c)? std::ios::in 是否小于 std::ios::app?有关系吗?无论如何,关系运算符总是为枚举类型和整数类型定义的。

    要回答主要问题,您可以将&amp; 实现为重载的非成员 函数:

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

    我相信位掩码类型所需的所有操作都可以为作用域枚举定义,但不是这样的要求

    Ci & Cj 是非零且 Ci & Cj 是零

    和:

    在对象X中设置Y的值是表达式X & Y非零。

    由于作用域枚举不支持隐式转换为整数类型,因此您无法可靠地测试它是否为非零。你需要写if ((X&amp;Y) != bitmask{}),我不认为这是委员会的意图。

    (我最初认为它们可用于定义位掩码类型,然后记得我曾尝试使用范围枚举来实现一个,但在测试零/非零时遇到了问题。)

    编辑:我刚刚记得std::launch 是一个作用域枚举类型和一个位掩码类型......所以显然作用域枚举可以是位掩码类型!

    【讨论】:

      【解决方案5】:

      下面是枚举标志的简短示例。

      #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) 是一个宏,在enum_flags.h 中定义(少于 100 行,免费使用,没有限制)。

      【讨论】:

      • 每次都记录样板的救生员!
      • 看起来你有一个错误。第 68 行应该说 enum class T: INT_T;而不是枚举类 T;否则 C3433: 'eee': 枚举的所有声明必须具有相同的基础类型
      • 这非常有用。问题:你为什么使用intptr_t 而不是例如uint32_t?
      • @j intptr_t 匹配机器寄存器大小。现代机器是 64 位的,旧机器是 32 位的,如果你在一些奇特的平台上使用它甚至可以是 16 位,所以 intptr_t 似乎是枚举默认类型的最佳候选者。但是您可能已经注意到,还有一个宏可以将类型作为参数
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多