C ++枚举标志与bitset

时间:2017-08-07 07:45:43

标签: c++ enums bitset

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

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具有更方便的set / clear / flip函数来处理,但枚举标志的使用是一种更广泛的方法。

我可以在日常代码中使用什么可能的位集内容以及什么时候使用?

4 个答案:

答案 0 :(得分:5)

std::bitset和c-style 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风格enumstd::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 & FlagState::Read) == FlagState::Read)

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

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

答案 1 :(得分:2)

您是否使用优化进行编译?速度因子不太可能是24倍。

对我来说,bitset是优越的,因为它为你管理空间:

  • 可以根据需要进行扩展。如果您有很多标记,则int / long long版本可能会空间不足。
  • 可能占用更少的空间,如果你只使用几个标志(它可以放在unsigned char / unsigned short - 我不确定实现是否适用于此优化,但是)

答案 2 :(得分:0)

(广告模式开启) 您可以同时获得:便捷的界面和最高性能。而且类型安全也是如此。 https://github.com/oliora/bitmask

答案 3 :(得分:0)

一些观察结果:

  • std::bitset< N >支持任意数量的位(例如,多于64位),而基础整数类型的枚举被限制为64位;
  • std::bitset< N >可以隐式地(取决于std实现)使用具有适合请求的位数的最小大小的基础整数类型,而枚举的基础整数类型需要明确声明(否则) ,int将用作默认的基础整数类型);
  • std::bitset< N >代表N位的通用序列,而作用域枚举提供类型安全性,可用于方法重载;
  • 如果将std::bitset< N >用作位掩码,则典型实现取决于索引(!=掩码)目的的其他枚举类型;

请注意,为方便起见,可以将后两个观察值组合以定义强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< N >(例如auto flags = Depth | Stencil;);
  • 枚举类型同时支持有符号和无符号基础整数类型,而std::bitset< N >在内部使用无符号整数类型(移位运算符)。

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