键入安全枚举位标志

Type safe enum bit flags

本文关键字:标志 枚举 安全      更新时间:2023-10-16

我希望为当前的问题使用一组位标志。这些标志(很好地(被定义为enum的一部分,但是我知道当您从枚举中OR两个值时,OR操作的返回类型具有int类型。

目前正在寻找的是一种解决方案,它将允许位掩码的用户保持类型安全,因此我为operator |创建了以下重载

enum ENUM
{
    ONE     = 0x01,
    TWO     = 0x02,
    THREE   = 0x04,
    FOUR    = 0x08,
    FIVE    = 0x10,
    SIX     = 0x20
};
ENUM operator | ( ENUM lhs, ENUM rhs )
{
    // Cast to int first otherwise we'll just end up recursing
    return static_cast< ENUM >( static_cast< int >( lhs ) | static_cast< int >( rhs ) );
}
void enumTest( ENUM v )
{
}
int main( int argc, char **argv )
{
    // Valid calls to enumTest
    enumTest( ONE | TWO | FIVE );
    enumTest( TWO | THREE | FOUR | FIVE );
    enumTest( ONE | TWO | THREE | FOUR | FIVE | SIX );
    return 0;
}

这种重载真的提供类型安全吗?强制转换包含枚举中未定义的值的int是否会导致未定义的行为?有什么需要注意的吗?

这种重载真的提供类型安全吗?

在这种情况下,是的。枚举的有效值范围至少达到(但不一定包括(最大命名枚举器之后的下一个最大 2 的幂,以便允许将其用于此类位掩码。因此,对两个值的任何按位运算都将给出可由此类型表示的值。

强制转换包含枚举中未定义的值的 int 是否会导致未定义的行为?

否,只要值可由枚举表示,它们就在此处。

有什么需要注意的吗?

如果您正在执行算术等操作,这可能会使值超出范围,那么您将获得实现定义的结果,但不会获得未定义的行为。

如果你考虑类型安全,最好使用 std::bitset

enum BITS { A, B, C, D }; 
std::bitset<4> bset, bset1;
bset.set(A); bset.set(C);
bset1[B] = 1;
assert(bset[A] == bset[C]);
assert(bset[A] != bset[B]);
assert(bset1 != bset);

常量的值在 OR 下不闭合。换句话说,两个枚举常量的 OR 的结果可能会导致一个不是 ENUM 常量的值:

0x30 == FIVE | SIX;

该标准说这是可以的,一个 enumaration 的值可以不等于它的任何 enumarator(常量(。大概是为了允许这种类型的用法。

在我看来,这不是类型安全的,因为如果您要查看enumTest的实现,您必须意识到参数类型是ENUM,但它的值可能不是ENUM枚举器。

我认为,如果这些只是位标志,那么按照编译器的要求去做:使用int来组合标志。

使用像

您这样的简单enum

enum ENUM
{
    ONE     = 0x01,
    TWO     = 0x02,
    ...
};

它是实现定义的基础类型(很可能是int(1,但只要要使用|(按位或(来创建掩码,结果就永远不会需要比此枚举的最大值更宽的类型。


[1]"枚举的基础类型是一个整数类型,可以表示枚举中定义的所有枚举器值。它是实现定义的,哪个整型类型用作枚举的基础类型,但基础类型不得大于int,除非枚举器的值无法容纳在intunsigned int中。

这是我使用位标志的方法:

template<typename E>
class Options {
      unsigned long values;
      constexpr Options(unsigned long v, int) : values{v} {}
   public:
      constexpr Options() : values(0) {}
      constexpr Options(unsigned n) : values{1UL << n} {}
      constexpr bool operator==(Options const& other) const {
         return (values & other.values) == other.values;
      }
      constexpr bool operator!=(Options const& other) const {
         return !operator==(other);
      }
      constexpr Options operator+(Options const& other) const {
         return {values | other.values, 0};
      }
      Options& operator+=(Options const& other) {
         values |= other.values;
         return *this;
      }
      Options& operator-=(Options const& other) {
         values &= ~other.values;
         return *this;
      }
};
#define DECLARE_OPTIONS(name) class name##__Tag; using name = Options
#define DEFINE_OPTION(name, option, index) constexpr name option(index)

您可以像这样使用它:

DECLARE_OPTIONS(ENUM);
DEFINE_OPTIONS(ENUM, ONE, 0);
DEFINE_OPTIONS(ENUM, TWO, 1);
DEFINE_OPTIONS(ENUM, THREE, 2);
DEFINE_OPTIONS(ENUM, FOUR, 3);

那么ONE + TWO仍然是 ENUM 型。您可以重用该类来定义具有不同、不兼容类型的多个位标志集。

我个人不喜欢使用|&来设置和测试位。这是需要执行的逻辑运算来设置和测试,但除非您考虑按位运算,否则它们不会表达操作的含义。如果你读出ONE | TWO你可能会认为你想要一个或两个,不一定同时需要两个。这就是为什么我更喜欢使用 + 将标志添加在一起,==来测试是否设置了标志。

有关我建议的实现的更多详细信息,请参阅此博客文章。