追求更好的位标志枚举

21

好的,所以我们现在已经到了 C++ 17,但仍然没有一个令人满意的位标志界面。

我们有 enum,它会将成员值流入封闭范围,但会隐式地转换为其基础类型,因此可以像位标志一样使用,但拒绝在不进行强制转换的情况下重新分配回枚举。

我们有< code> enum class ,它解决了名称空间问题,使它们的值必须显式命名为 MyEnum :: MyFlag ,甚至是 MyClass :: MyEnum :: MyFlag ,但它们不能隐式转换为其基础类型,因此无法用作位标志而不需要无休止地来回转换。

最后,我们有来自C 的旧位字段,例如:

struct FileFlags {
   unsigned ReadOnly : 1;
   unsigned Hidden : 1;
   ...
};
具有初始化整个值的好方法缺乏,必须使用memset或转换地址或类似操作覆盖整个值或一次性初始化它,或者以其他方式同时操纵多个位,这是其缺点之一。此外,它也无法为给定标志的值命名,而不是其地址,因此没有表示0x02的名称,而在使用枚举时存在这样的名称,因此对于位字段来说很难命名组合标志,例如FileFlags::ReadOnly | FileFlags::Hidden - 没有一种好的方式来表示这些。

此外,我们仍然可以使用简单的constexpr#define来命名位值,然后根本不使用枚举。这种方法有效,但会将位值完全与底层位标志类型分离。也许这最终并不是最糟糕的方法,尤其是如果位标志值是在结构体内成为constexpr以赋予它们自己的命名空间?

struct FileFlags {
    constexpr static uint16_t ReadOnly = 0x01u;
    constexpr static uint16_t Hidden = 0x02u;
    ...
}
目前我们有许多技术,但没有一种真正可靠的方法来表达所需的实体。这是一个具有以下有效位标志的类型,它有自己的名称空间,这些位和类型应该可以自由地使用标准的按位运算符(例如| & ^ ~),并且它们应该可以与像0这样的整数值进行比较,任何按位运算符的结果都应该保持命名类型,而不是演变为整数。尽管如此,在C++中还是有一些尝试来产生上述实体的代码。其中包括:
1. Windows操作系统团队开发了一个简单的宏,用于生成定义给定枚举类型上必要缺失运算符的C++代码DEFINE_ENUM_FLAG_OPERATORS(EnumType),该宏定义了操作符 | & ^ ~ 和相关赋值操作符,如|=等。 2. 'grisumbras'在公共GIT项目中提供了启用带作用域的枚举的位标志语义here,使用enable_if元编程使得给定的枚举可以转换为支持缺少运算符的位标志类型,然后再次无声地转换回来。 3. 我写了一个相对简单的bit_flags包装器,它在自身上定义了所有按位运算符,以便可以使用bit_flags<EnumType> flags,然后flags具有位标志语义。但是,这种方法无法使枚举基本类型实际上直接处理位运算符,因此即使在使用bit_flags<EnumType>时也不能说EnumType::ReadOnly | EnumType::Hidden,因为底层枚举本身仍不支持必要的操作符。我最终不得不做与#1和#2类似的事情,并通过要求用户为其枚举声明元类型的特化,例如template <> struct is_bitflag_enum<EnumType> : std::true_type {};,来启用各种按位运算符的operator | (EnumType, EnumType)
最后,虽然可以将位标志定义在所属类之外,以便在客户端类中调用"使此枚举成为按位类型",以完全使用该功能,但现在位标志在外部范围而不是与该类本身相关联。因此,这些问题仍然需要解决,但它们都没有到达无法解决的地步。可能还有其他方法允许在封闭类作用域内声明使用按位运算符的枚举,但我目前没有发现。是否有人有进一步的想法或尚未考虑的方法,可以让我在这个问题上获得"最佳选择"?

1
也许我对位运算语义有不同的理解,但是 std::bitset 支持所有的位运算符。 - François Andrieux
1
这不是我想象的那样 - 你说得对 - 它使位运算成为可能。但是,仍然没有名字作用域的标志名称集合 - 仍然需要在外部设置它们,并且它们与该类型根本没有语义关系... 不是吗? - Mordachai
2
@FrançoisAndrieux std::bitset 可能在大小方面浪费。如果我有六个位标志,我可能不希望相应的类型占用 64 位。但例如 MSVC 和 gcc 目前就是这样做的,因为在这些实现中 std::bitset 是由一个整数数组支持的(尽管所需大小是模板参数)。我明白了,库可以改进这一点,但作为用户,如果他们没有这样做,你可能会被撇在一边。 - Max Langhof
3
总会有提出标准提案的选项。然后也许在 C++20 中你就能得到完美的解决方案 :-) - Jesper Juhl
1
我使用 https://dev59.com/6XM_5IYBdhLWcg3wRw51#31906371 / https://codereview.stackexchange.com/q/96146/2503。 - Lightness Races in Orbit
显示剩余11条评论
5个回答

1
你可以在封闭类中拥有将枚举作为值的友元函数。这可以在宏内部使用,以定义必要的函数,全部在类范围内。
例如,为了避免特化is_bitflag_enum trait,可以专门为持有枚举和运算符的结构进行特化。这就像#2,但仍无法在类中完成。
#include <type_traits>

template<class Tag>
struct bitflag {
    enum class type;

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr type operator OP(type lhs, type rhs) noexcept { \
        typedef typename ::std::underlying_type<type>::type underlying; \
        return static_cast<type>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr type& operator OP ## = (type& lhs, type rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(|)
    DEFINE_BITFLAG_OPERATOR(&)
    DEFINE_BITFLAG_OPERATOR(^)

#undef DEFINE_BITFLAG_OPERATOR

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr bool operator OP(type lhs, typename ::std::underlying_type<type>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<type>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<type>::type lhs, type rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<type>::type>(rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(==)
    DEFINE_BITFLAG_OPERATOR(!=)
    DEFINE_BITFLAG_OPERATOR(<)
    DEFINE_BITFLAG_OPERATOR(>)
    DEFINE_BITFLAG_OPERATOR(>=)
    DEFINE_BITFLAG_OPERATOR(<=)

#undef DEFINE_BITFLAG_OPERATOR

    friend constexpr type operator~(type e) noexcept {
        return static_cast<type>(~static_cast<typename ::std::underlying_type<type>::type>(e));
    }

    friend constexpr bool operator!(type e) noexcept {
        return static_cast<bool>(static_cast<typename ::std::underlying_type<type>::type>(e));
    }
};

// The `struct file_flags_tag` (Which declares a new type) differentiates between different
// enum classes declared
template<> enum class bitflag<struct file_flags_tag>::type {
    none = 0,
    readable = 1 << 0,
    writable = 1 << 1,
    executable = 1 << 2,
    hidden = 1 << 3
};

using file_flags = bitflag<file_flags_tag>::type;

bool is_executable(file_flags f) {
    return (f & file_flags::executable) == 0;
}

你也可以编写一个宏来定义每一个友元函数。这就像#1,但是所有内容都在类作用域内。
#include <type_traits>

#define MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(OP, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator OP(ENUM_TYPE lhs, ENUM_TYPE rhs) noexcept { \
        typedef typename ::std::underlying_type<ENUM_TYPE>::type underlying; \
        return static_cast<ENUM_TYPE>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr ENUM_TYPE& operator OP ## = (ENUM_TYPE& lhs, ENUM_TYPE rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

#define MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(OP, ENUM_TYPE) \
    friend constexpr bool operator OP(ENUM_TYPE lhs, typename ::std::underlying_type<ENUM_TYPE>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<ENUM_TYPE>::type lhs, ENUM_TYPE rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(rhs); \
    }


#define MAKE_BITFLAG_FRIEND_OPERATORS(ENUM_TYPE) \
    public: \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(|, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(&, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(^, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(==, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(!=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<=, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator~(ENUM_TYPE e) noexcept { \
        return static_cast<ENUM_TYPE>(~static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    } \
    friend constexpr bool operator!(ENUM_TYPE e) noexcept { \
        return static_cast<bool>(static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    }

// ^ The above in a header somewhere

class my_class {
public:
    enum class my_flags {
        none = 0, flag_a = 1 << 0, flag_b = 1 << 2
    };

    MAKE_BITFLAG_FRIEND_OPERATORS(my_flags)

    bool has_flag_a(my_flags f) {
        return (f & my_flags::flag_a) == 0;
    }
};

0

使用自己的位集实现底层整数类型选择并不困难。枚举的问题在于缺少适应位集所必需的元信息。但是,通过适当的元编程和标志启用特性,仍然可以拥有这样的语法:

flagset<file_access_enum>   rw = bit(read_access_flag)|bit(write_access_flag);

@Red.Wave 可变参数模板是个好主意。事实上,那是我对 Xaqq 的解决方案所做的修改之一(请看我的回答)! - Lightness Races in Orbit
@LightnessRacesinOrbit 底层类型很昂贵,不适合使用。正确的实现方式类似于 std::bitsetenm::end-enm::start。然而,理想情况下是像 std::bitset<number_of_values<enm>()> 这样的实现,但这似乎是不可能实现的。 - Red.Wave
@LightnessRacesinOrbit 所需位数与底层类型无关。它仅受值范围或枚举元素数量的限制,两者通常都相对较小。 - Red.Wave
假设char作为底层类型,有256个可能的值,这意味着bitset<256>,但实际枚举数量要少得多;比如在Linux上文件访问权限只有11种。11位(2字节)与256位(32字节)相比,如果你看不出区别,我也无法解释了。 - Red.Wave
在这种情况下,是否应该每个位存储一个标志 - 而将一个标志存储在每个字节甚至每个int32中要容易得多?显然的答案是大小考虑。您是否曾经使用过atd :: vector <bool>?许多人抱怨它应该是面向字节而不是位。为什么boost浪费了这么多人力资源来定义各种bitset类型?拥有更多的硬件资源能力并不是浪费它们的借口。 - Red.Wave
显示剩余13条评论

0

我采用了Xaqq在Code Review SE上的FlagSet方法

关键是引入一个新类型作为固定选项列表中一个或多个开启值的“容器”。该容器是一个包装bitset的类,它以作用域枚举的实例作为输入。

由于使用了作用域枚举,因此它是类型安全的,并且可以通过运算符重载委托到位集操作来执行类似于按位的操作。如果您希望,仍然可以直接使用作用域枚举,如果您不需要按位操作或存储多个标志。

对于生产环境,我对链接代码进行了一些更改;其中一些在Code Review页面的评论中讨论过。


0

我使用带有以下模板运算符的enum class

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM operator |( ENUM lhs, ENUM rhs )
{
    return static_cast< ENUM >( static_cast< UInt32 >( lhs ) | static_cast< UInt32 >( rhs ));
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator |=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs | rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline UInt32 operator &( ENUM lhs, ENUM rhs )
{
    return static_cast< UInt32 >( lhs ) & static_cast< UInt32 >( rhs );
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs & rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, int rhs )
{
    lhs = static_cast< ENUM >( static_cast< int >( lhs ) & rhs );
    return lhs;
}

如果您担心上述运算符泄漏到其他枚举中,我想您可以将它们封装在声明枚举的同一命名空间中,甚至只是按枚举实现它们(我曾经使用宏来实现)。但总的来说,我认为这是过度设计了,现在我已经将它们声明在我的顶层命名空间中,以供任何代码使用。


0
例如。
// union only for convenient bit access. 
typedef union a
{ // it has its own name-scope
    struct b
     {
         unsigned b0 : 1;
         unsigned b2 : 1;
         unsigned b3 : 1;
         unsigned b4 : 1;
         unsigned b5 : 1;
         unsigned b6 : 1;
         unsigned b7 : 1;
         unsigned b8 : 1;
         //...
     } bits;
    unsigned u_bits;
    // has the following valid bit-flags in it
    typedef enum {
        Empty = 0u,
        ReadOnly = 0x01u,
        Hidden  = 0x02u
    } Values;
    Values operator =(Values _v) { u_bits = _v; return _v; }
     // should be freely usable with standard bitwise operators such as | & ^ ~   
    union a& operator |( Values _v) { u_bits |= _v; return *this; }
    union a& operator &( Values _v) { u_bits &= _v; return *this; }
    union a& operator |=( Values _v) { u_bits |= _v; return *this; }
    union a& operator &=( Values _v) { u_bits &= _v; return *this; }
     // ....
    // they should be comparable to integral values such as 0
    bool operator <( unsigned _v) { return u_bits < _v; }
    bool operator >( unsigned _v) { return u_bits > _v; }
    bool operator ==( unsigned _v) { return u_bits == _v; }
    bool operator !=( unsigned _v) { return u_bits != _v; }
} BITS;


int main()
 {
     BITS bits;
     int integral = 0;

     bits = bits.Empty;

     // they should be comparable to integral values such as 0
     if ( bits == 0)
     {
         bits = bits.Hidden;
         // should be freely usable with standard bitwise operators such as | & ^ ~
         bits = bits | bits.ReadOnly;
         bits |= bits.Hidden;
         // the result of any bitwise operators should remain the named type, and not devolve into an integral
         //bits = integral & bits; // error
         //bits |= integral; // error
     }
 }

为什么有人要在位集上使用<>运算符? - Roland Illig
@RolandIllig 这只涵盖了评论“应该与整数值相当”的任务。当然,它可能永远不会被消费者使用,但为了履行义务而包括在内。实际上,这只是演示的样本,可以改进。 - Andrey Sv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接