类型安全的枚举位标志

12

我希望使用一组位标志解决当前问题。这些标志被(良好地)定义为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强制转换,会导致未定义的行为吗?还有需要注意的任何警告吗?


operator|(FIVE, SIX) == 0x30。哪个ENUM常量的值为0x30 - Adam
我不会对结果值进行任何形式的比较,我只会检查标志。 - ctor
1
那么 OR 的结果应该保持为 int。 - Adam
1
@Adam:为什么?OR的结果对于这个枚举来说是一个有效的值。 - Mike Seymour
顺便提一下,Qt使用模板QFlags<>实现了OP尝试做的事情,你可能想看一下这个链接:http://qt-project.org/doc/qt-5.0/qtcore/qflags.html。 - drescherjm
5个回答

6
这种过载确实提供了类型安全吗?
在这种情况下,是的。枚举值的有效范围至少延伸到(但不一定包括)最大命名枚举器之后的下一个最大二次幂,以便像这样用于位掩码。因此,对两个值进行任何按位操作将给出此类型可以表示的值。
将包含未在枚举中定义的值的int强制转换是否会导致未定义的行为?
否,只要这些值可以由枚举表示,它们就在这里。
需要注意什么问题吗?
如果您正在执行诸如算术等可能使值超出范围的操作,则会得到一个实现定义的结果,但不会出现未定义的行为。

“只要这些值可以由枚举类型表示”,您的意思是这些值必须可以由枚举底层类型表示吗? - Carlton

4

如果你考虑类型安全,最好使用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);

3

你的常量值不符合OR运算的封闭性。换句话说,两个枚举常量进行OR运算可能会得到一个不是枚举常量的值:

0x30 == FIVE | SIX;

标准规定这是可以的,一个枚举可以有一个值不等于任何一个其枚举器(常量)。可能是为了允许这种类型的用法。
在我看来,这不是类型安全的,因为如果你要查看 enumTest 的实现,你必须知道参数类型是 ENUM ,但它可能有一个值,而这个值不是 ENUM 枚举器。
我认为,如果这些只是比特标志,那么按照编译器想要的方式去做:使用 int 来组合标志。

1
只要结果是有效的枚举值(就像在这里一样),转换就被明确定义了。有效值不仅限于命名的枚举器,而是包含(至少)一切小于下一个最大二次幂的值 - 这是为了允许恰好这种类型的用法而进行规定的。 - Mike Seymour
@MikeSeymour 你有相关的参考资料吗?我查到的所有信息都说这是非法的。 - Adam
C++11 7.2。第7段最相关,因为它定义了值的范围。 - Mike Seymour
@MikeSeymour 用户发帖时特别询问 C++03。 - Adam
@MikeSeymour 我猜你指的是这句话:“可以定义一个枚举,它的值不被任何枚举器定义。” - Adam
在C++03中,以及过去几十年的其他C和C ++版本中都是一样的。我只能为您提供对C++11的参考,因为我没有历史语言规范库可供参考。我指的是定义允许值范围的整个段落;但是该句子特别与此答案相关。 - Mike Seymour

2

使用类似于您的简单enum:

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

在实现中,底层类型是未定义的(很可能是int1,但只要您使用 |(按位或)来创建掩码,结果将永远不需要比此枚举中的最大值更宽的类型。


[1] "枚举的底层类型是一个整数类型,可以表示枚举中定义的所有枚举器值。除了底层类型不能大于int,除非枚举器的值无法适应intunsigned int之外,哪种整数类型用作枚举的底层类型是未定义的。"


1

这是我对比特标志的处理方法:

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,您可能会认为您想要ONE或TWO,而不一定是两者都要。这就是为什么我更喜欢使用+将标志添加在一起,使用==测试是否设置了标志的原因。
有关我建议的实现的更多详细信息,请参见此博客文章

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