C ++枚举标志与位集

Posted

技术标签:

【中文标题】C ++枚举标志与位集【英文标题】:C++ enum flags vs bitset 【发布时间】:2018-01-14 10:56:24 【问题描述】:

使用位集优于枚举标志的优点/缺点是什么?

namespace Flag 
    enum State 
        Read   = 1 << 0,
        Write  = 1 << 1,
        Binary = 1 << 2,
    ;


namespace Plain 
    enum State 
        Read,
        Write,
        Binary,
        Count
    ;


int main()

    
        unsigned int state = Flag::Read | Flag::Binary;
        std::cout << state << std::endl;

        state |= Flag::Write;
        state &= ~(Flag::Read | Flag::Binary);
        std::cout << state << std::endl;
     
        std::bitset<Plain::Count> state;
        state.set(Plain::Read);
        state.set(Plain::Binary);
        std::cout << state.to_ulong() << std::endl;

        state.flip();
        std::cout << state.to_ulong() << std::endl;
    

    return 0;

到目前为止,我可以看到,位集有更方便的设置/清除/翻转功能来处理,但枚举标志的使用是一种更广泛的方法。

bitset 有哪些可能的缺点,我应该在日常代码中使用什么以及何时使用?

【问题讨论】:

由于flags是预先计算好的,在你的测试中具有明显的优势。 我会说这一切都取决于。它取决于用例、个人偏好、项目要求、使用的代码风格指南等等。如果是为了你自己的项目,那就随心所欲。不过我的建议是,在性能之前,您首先要考虑诸如可读性、可维护性和正确性之类的问题。 “足够好”通常足够好。 bitset 可以与 constexpr 一起使用吗?你可能会在那里得到相同的时间。但总体而言,bitset 速度较慢,因为它与平台无关。 bitsets are significally slower (~24 times on my machine) than bare bit operations 我还有另一个结果,bitsets 几乎和asm 代码一样快。 首先:这两个例子是等价的!您必须在显式翻转后设置读取和二进制标志才能真正获得等价。所以实际上,bitset 变体产生更长的代码(四行)......当然,并非总是更短的代码更好阅读。对我来说,因为我已经习惯了裸位操作,所以它和 bitset 变体一样容易阅读,因此,我更喜欢前者,但这是一个非常个人问题... 【参考方案1】:

std::bitset 和 c 风格的 enum 在管理标志方面都有重要的缺点。首先,让我们考虑以下示例代码:

namespace Flag 
    enum State 
        Read   = 1 << 0,
        Write  = 1 << 1,
        Binary = 1 << 2,
    ;


namespace Plain 
    enum State 
        Read,
        Write,
        Binary,
        Count
    ;


void f(int);
void g(int);
void g(Flag::State);
void h(std::bitset<sizeof(Flag::State)>);

namespace system1 
    Flag::State getFlags();

namespace system2 
    Plain::State getFlags();


int main()

    f(Flag::Read);  // Flag::Read is implicitly converted to `int`, losing type safety
    f(Plain::Read); // Plain::Read is also implicitly converted to `int`

    auto state = Flag::Read | Flag::Write; // type is not `Flag::State` as one could expect, it is `int` instead
    g(state); // This function calls the `int` overload rather than the `Flag::State` overload

    auto system1State = system1::getFlags();
    auto system2State = system2::getFlags();
    if (system1State == system2State)  // Compiles properly, but semantics are broken, `Flag::State`

    std::bitset<sizeof(Flag::State)> flagSet; // Notice that the type of bitset only indicates the amount of bits, there's no type safety here either
    std::bitset<sizeof(Plain::State)> plainSet;
    // f(flagSet); bitset doesn't implicitly convert to `int`, so this wouldn't compile which is slightly better than c-style `enum`

    flagSet.set(Flag::Read);    // No type safety, which means that bitset
    flagSet.reset(Plain::Read); // is willing to accept values from any enumeration

    h(flagSet);  // Both kinds of sets can be
    h(plainSet); // passed to the same function

尽管您可能认为这些问题在简单示例中很容易发现,但它们最终会蔓延到每个在 c 样式 enum 和 std::bitset 之上构建标志的代码库中。

那么,您可以做些什么来提高类型安全性?首先,C++11 的作用域枚举是对类型安全的改进。但这极大地阻碍了便利性。部分解决方案是对作用域枚举使用模板生成的按位运算符。这是一篇很棒的博客文章,它解释了它的工作原理并提供了工作代码:https://www.justsoftwaresolutions.co.uk/cplusplus/using-enum-classes-as-bitfields.html

现在让我们看看它会是什么样子:

enum class FlagState 
    Read   = 1 << 0,
    Write  = 1 << 1,
    Binary = 1 << 2,
;
template<>
struct enable_bitmask_operators<FlagState>
    static const bool enable=true;
;

enum class PlainState 
    Read,
    Write,
    Binary,
    Count
;

void f(int);
void g(int);
void g(FlagState);
FlagState h();

namespace system1 
    FlagState getFlags();

namespace system2 
    PlainState getFlags();


int main()

    f(FlagState::Read);  // Compile error, FlagState is not an `int`
    f(PlainState::Read); // Compile error, PlainState is not an `int`

    auto state = Flag::Read | Flag::Write; // type is `FlagState` as one could expect
    g(state); // This function calls the `FlagState` overload

    auto system1State = system1::getFlags();
    auto system2State = system2::getFlags();
    if (system1State == system2State)  // Compile error, there is no `operator==(FlagState, PlainState)`

    auto someFlag = h();
    if (someFlag == FlagState::Read)  // This compiles fine, but this is another type of recurring bug

此示例的最后一行显示了一个在编译时仍然无法捕获的问题。在某些情况下,比较平等可能是真正需要的。但大多数时候,真正的意思是if ((someFlag &amp; FlagState::Read) == FlagState::Read)

为了解决这个问题,我们必须区分枚举器的类型和位掩码的类型。这是一篇文章,详细介绍了我之前提到的部分解决方案的改进:https://dalzhim.github.io/2017/08/11/Improving-the-enum-class-bitmask/ 免责声明:我是这篇文章的作者。

使用上一篇文章中的模板生成的按位运算符时,您将获得我们在上一段代码中展示的所有好处,同时还发现了mask == enumerator 错误。

【讨论】:

【参考方案2】:

你编译优化了吗?几乎不可能有 24 倍的速度因子。

对我来说,bitset 更胜一筹,因为它为你管理空间:

可以根据需要进行扩展。如果您有很多标志,您可能会在int/long long 版本中用完空间。 如果只使用几个标志,可能会占用更少的空间(它可以放入 unsigned char/unsigned short - 不过我不确定实现是否应用了这种优化)

【讨论】:

【参考方案3】:

(广告模式开启) 您可以同时获得:方便的界面和最佳性能。还有类型安全。 https://github.com/oliora/bitmask

【讨论】:

这里有一个不同的选项:codereview.stackexchange.com/questions/183246/…【参考方案4】:

一些观察:

std::bitset&lt; N &gt; 支持任意位数(例如,超过 64 位),而底层整数类型的枚举仅限于 64 位; std::bitset&lt; N &gt; 可以隐式(取决于 std 实现)使用具有适合所请求位数的最小大小的底层整数类型,而枚举的底层整数类型需要显式声明(否则,将使用 int作为默认的基础整数类型); std::bitset&lt; N &gt; 表示 N 位的通用序列,而 scoped 枚举提供可用于方法重载的类型安全; 如果将std::bitset&lt; N &gt; 用作位掩码,则典型 实现取决于用于索引(!= 掩码)目的的附加枚举类型;

请注意,为方便起见,可以将后两个观察结果组合起来定义一个强 std::bitset 类型:

typename< Enum E, std::size_t N >
class BitSet : public std::bitset< N >

    ...

    [[nodiscard]]
    constexpr bool operator[](E pos) const;

    ...
;

如果代码支持某种反射来获得显式枚举值的个数,那么可以直接从枚举类型推导出位数。

作用域枚举类型没有按位运算符重载(可以很容易地使用 SFINAE 或所有作用域和非作用域枚举类型的概念定义一次,但需要在使用前包含)和非限定枚举类型将衰减到底层的整数类型; 枚举类型的按位运算符重载,需要的样板文件少于 std::bitset&lt; N &gt;(例如,auto flags = Depth | Stencil;); 枚举类型支持有符号和无符号基础整数类型,而std::bitset&lt; N &gt; 在内部使用无符号整数类型(移位运算符)。

FWIIW,在我自己的代码中,我主要使用std::bitset(和eastl::bitvector)作为private位/bool用于设置/获取单个位的容​​器/bools。对于屏蔽操作,我更喜欢具有显式定义的底层类型和按位运算符重载的作用域枚举类型。

【讨论】:

以上是关于C ++枚举标志与位集的主要内容,如果未能解决你的问题,请参考以下文章

Linux C 编程学习第五天_数据类型标志

枚举与位枚举

标志枚举

在 C 中指定枚举类型的大小

C++中标志位的几种实现方法

⭐C/C++の深入浅出⭐int数与多枚举值互转