在C ++中将作用域枚举用于位标志


60

一个enum X : int(C#)或enum class X : int(C ++ 11)是具有一个隐藏的内部场类型int,可容纳任何值。另外,X在枚举上定义了许多预定义的常量。可以将枚举转换为其整数值,反之亦然。在C#和C ++ 11中都是如此。

在C#中,按照Microsoft的建议,枚举不仅用于保存单个值,而且还用于保存标志的按位组合。此类枚举(通常但并非必须)用[Flags]属性修饰。为了简化开发人员的工作,按位运算符(OR,AND等)被重载,因此您可以轻松地执行以下操作(C#):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

我是一位经验丰富的C#开发人员,但是现在仅对C ++进行了几天的编程,并且不了解C ++约定。我打算以与C#中使用的完全相同的方式使用C ++ 11枚举。在C ++ 11中,作用域枚举上的按位运算符没有重载,因此我想重载它们

这引发了一场辩论,意见似乎在以下三种选择之间有所不同:

  1. 枚举类型的变量用于保存位字段,类似于C#:

    void M(NumericType flags);
    
    // With operator overloading:
    M(NumericType::Sign | NumericType::ZeroPadding);
    
    // Without operator overloading:
    M(static_cast<NumericType>(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding)));
    

    但这会违背C ++ 11范围枚举的强类型枚举哲学。

  2. 如果要存储枚举的按位组合,请使用纯整数:

    void M(int flags);
    
    M(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding));
    

    但这会将所有内容简化为int,从而使您不知道应该在方法中输入哪种类型。

  3. 编写一个单独的类,该类将重载运算符并将按位标志保存在隐藏的整数字段中:

    class NumericTypeFlags {
        unsigned flags_;
    public:
        NumericTypeFlags () : flags_(0) {}
        NumericTypeFlags (NumericType t) : flags_(static_cast<unsigned>(t)) {}
        //...define BITWISE test/set operations
    };
    
    void M(NumericTypeFlags flags);
    
    M(NumericType::Sign | NumericType::ZeroPadding);
    

    全码user315052

    但是,那么您就没有IntelliSense或任何支持来提示您可能的值。

我知道这是一个主观问题,但是:我应该使用哪种方法?哪种方法(如果有)在C ++中得到最广泛的认可?在处理位字段时,您使用什么方法?为什么

当然,由于所有这三种方法都有效,因此我正在寻找事实和技术原因,公认的惯例,而不仅仅是个人喜好。

例如,由于我的C#背景,我倾向于使用C ++中的方法1。这还有一个好处,就是我的开发环境可以向我提示可能的值,并且使用重载的枚举运算符可以很容易编写和理解,而且很干净。方法签名清楚地表明了期望的价值。但是这里的大多数人都不同意我,这可能是有充分理由的。


2
ISO C ++委员会发现选项1非常重要,足以明确声明枚举的值范围包括标志的所有二进制组合。(这早于C ++ 03)所以这个主观的问题得到了客观的认可。
MSalters 2015年

1
(为澄清@MSalters的评论,C ++枚举的范围是基于其基础类型(如果是固定类型),或者是基于其枚举数。在后一种情况下,该范围是基于可以容纳所有已定义枚举数的最小位域;例如,for enum E { A = 1, B = 2, C = 4, };的范围是0..7(3位)。因此,C ++标准明确保证#1将始终是一个可行的选择。(特别是,除非另有说明,否则enum class默认为enum class : int,因此始终具有固定的基础类型。])
贾斯汀时间

Answers:


31

最简单的方法是让操作员自己超载。我正在考虑创建一个宏来扩展每种类型的基本重载。

#include <type_traits>

enum class SBJFrameDrag
{
    None = 0x00,
    Top = 0x01,
    Left = 0x02,
    Bottom = 0x04,
    Right = 0x08,
};

inline SBJFrameDrag operator | (SBJFrameDrag lhs, SBJFrameDrag rhs)
{
    using T = std::underlying_type_t <SBJFrameDrag>;
    return static_cast<SBJFrameDrag>(static_cast<T>(lhs) | static_cast<T>(rhs));
}

inline SBJFrameDrag& operator |= (SBJFrameDrag& lhs, SBJFrameDrag rhs)
{
    lhs = lhs | rhs;
    return lhs;
}

(请注意,这type_traits是C ++ 11标头,并且std::underlying_type_t是C ++ 14功能。)


6
std :: underlying_type_t是C ++ 14。可以在C ++ 11中使用std :: underlying_type <T> :: type。
ddevienne

14
您为什么在这里使用static_cast<T>输入,而在结果中使用C样式转换?
Ruslan

2
@Ruslan我是第二个问题
audiFanatic '16

当您已经知道std :: underlying_type_t是int时,为什么还要打扰呢?
poizan42

1
如果SBJFrameDrag在类中定义了-并且|以后在同一类的定义中使用了-operator,那么您将如何定义运算符,使其可以在类中使用?
HelloGoodbye 18/12/19

6

从历史上看,我总是会使用旧的(弱类型的)枚举来命名位常量,而只使用显式的存储类来存储结果标志。在这里,我有责任确保我的枚举适合存储类型,并跟踪该字段及其相关常数之间的关联。

我喜欢强类型枚举的想法,但是对于枚举类型的变量可能包含不在该枚举常量中的值的想法,我并不满意。

例如,假设按位或已被重载:

enum class E1 { A=1, B=2, C=4 };
void test(E1 e) {
    switch(e) {
    case E1::A: do_a(); break;
    case E1::B: do_b(); break;
    case E1::C: do_c(); break;
    default:
        illegal_value();
    }
}
// ...
test(E1::A); // ok
test(E1::A | E1::B); // nope

对于第三个选项,您需要一些样板来提取枚举的存储类型。假设我们想强制使用无符号的基础类型(我们也可以处理带符号的代码,还有更多代码):

template <size_t Size> struct IntegralTypeLookup;
template <> struct IntegralTypeLookup<sizeof(int64_t)> { typedef uint64_t Type; };
template <> struct IntegralTypeLookup<sizeof(int32_t)> { typedef uint32_t Type; };
template <> struct IntegralTypeLookup<sizeof(int16_t)> { typedef uint16_t Type; };
template <> struct IntegralTypeLookup<sizeof(int8_t)>  { typedef uint8_t Type; };

template <typename IntegralType> struct Integral {
    typedef typename IntegralTypeLookup<sizeof(IntegralType)>::Type Type;
};

template <typename ENUM> class EnumeratedFlags {
    typedef typename Integral<ENUM>::Type RawType;
    RawType raw;
public:
    EnumeratedFlags() : raw() {}
    EnumeratedFlags(EnumeratedFlags const&) = default;

    void set(ENUM e)   { raw |=  static_cast<RawType>(e); }
    void reset(ENUM e) { raw &= ~static_cast<RawType>(e); };
    bool test(ENUM e) const { return raw & static_cast<RawType>(e); }

    RawType raw_value() const { return raw; }
};
enum class E2: uint8_t { A=1, B=2, C=4 };
typedef EnumeratedFlags<E2> E2Flag;

这仍然不能为您提供IntelliSense或自动补全功能,但是存储类型检测比我最初预期的要难看。


现在,我确实找到了另一种选择:您可以为弱类型的枚举指定存储类型。它甚至具有与C#中相同的语法

enum E4 : int { ... };

因为它是弱类型的,并且隐式地从int(或您选择的任何存储类型)转换为int(或从int转换为int),所以拥有与枚举常量不匹配的值显得不太奇怪。

不利的一面是,这被描述为“过渡性的”。

注意 此变体将其枚举常量添加到嵌套作用域和封闭作用域中,但是您可以使用命名空间来解决此问题:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A

1
弱类型枚举的另一个缺点是它们的常量会污染我的名称空间,因为它们不需要以枚举名称为前缀。如果您有两个具有相同名称的成员的不同枚举,则这也可能导致各种奇怪的行为。
Daniel AA Pelsmaeker

确实如此。具有指定存储类型的弱类型变量将其常数添加到封闭范围其自身范围iiuc中。
无用的

无作用域的枚举器仅在周围的范围内声明。能够通过enum-name限定它是查找规则的一部分,而不是声明的一部分。C ++ 11 7.2 / 10:在立即包含enum-specifier的范围中声明了每个枚举名称和每个未作用域的枚举器。每个作用域枚举器都在枚举范围内声明。这些名称遵守为(3.3)和(3.4)中的所有名称定义的范围规则。
Lars Viklund 2014年

1
在C ++ 11中,我们有std :: underlying_type提供枚举的基础类型。所以我们有'template <typename IntegralType> struct Integral {typedef typename std :: underlying_type <IntegralType> :: type Type; }; 在C ++ 14中,这些甚至更简化为'template <typename IntegralType> struct Integral {typedef std :: underlying_type_t <IntegralType> Type; };
emsr

4

您可以使用来在C ++ 11中定义类型安全的枚举标志std::enable_if。这是一个基本的实现,可能缺少一些内容:

template<typename Enum, bool IsEnum = std::is_enum<Enum>::value>
class bitflag;

template<typename Enum>
class bitflag<Enum, true>
{
public:
  constexpr const static int number_of_bits = std::numeric_limits<typename std::underlying_type<Enum>::type>::digits;

  constexpr bitflag() = default;
  constexpr bitflag(Enum value) : bits(1 << static_cast<std::size_t>(value)) {}
  constexpr bitflag(const bitflag& other) : bits(other.bits) {}

  constexpr bitflag operator|(Enum value) const { bitflag result = *this; result.bits |= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator&(Enum value) const { bitflag result = *this; result.bits &= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator^(Enum value) const { bitflag result = *this; result.bits ^= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator~() const { bitflag result = *this; result.bits.flip(); return result; }

  constexpr bitflag& operator|=(Enum value) { bits |= 1 << static_cast<std::size_t>(value); return *this; }
  constexpr bitflag& operator&=(Enum value) { bits &= 1 << static_cast<std::size_t>(value); return *this; }
  constexpr bitflag& operator^=(Enum value) { bits ^= 1 << static_cast<std::size_t>(value); return *this; }

  constexpr bool any() const { return bits.any(); }
  constexpr bool all() const { return bits.all(); }
  constexpr bool none() const { return bits.none(); }
  constexpr operator bool() { return any(); }

  constexpr bool test(Enum value) const { return bits.test(1 << static_cast<std::size_t>(value)); }
  constexpr void set(Enum value) { bits.set(1 << static_cast<std::size_t>(value)); }
  constexpr void unset(Enum value) { bits.reset(1 << static_cast<std::size_t>(value)); }

private:
  std::bitset<number_of_bits> bits;
};

template<typename Enum>
constexpr typename std::enable_if<std::is_enum<Enum>::value, bitflag<Enum>>::type operator|(Enum left, Enum right)
{
  return bitflag<Enum>(left) | right;
}
template<typename Enum>
constexpr typename std::enable_if<std::is_enum<Enum>::value, bitflag<Enum>>::type operator&(Enum left, Enum right)
{
  return bitflag<Enum>(left) & right;
}
template<typename Enum>
constexpr typename std::enable_if_t<std::is_enum<Enum>::value, bitflag<Enum>>::type operator^(Enum left, Enum right)
{
  return bitflag<Enum>(left) ^ right;
}

请注意,number_of_bits不幸的是,编译器无法填写,因为C ++没有任何方法可以对枚举的可能值进行内省。

编辑:实际上我已经纠正,有可能number_of_bits为您填充编译器。

请注意,这可以(非常低效地)处理非连续的枚举值范围。只是说,将上述内容与这样的枚举一起使用不是一个好主意,否则会引起疯狂:

enum class wild_range { start = 0, end = 999999999 };

但是所有事情最终都认为这是一个非常有用的解决方案。不需要任何用户方面的摆弄,是类型安全的,并且在其范围内,尽可能地高效(我在std::bitset这里非常依赖于实现质量;))。


我确定我错过了一些运营商的负担。
rubenvb

2

一世 讨厌 在我的C ++ 14中对宏的厌恶程度与对下一个家伙的厌恶程度相同,但是我已经习惯了在各处使用它,而且也相当自由:

#define ENUM_FLAG_OPERATOR(T,X) inline T operator X (T lhs, T rhs) { return (T) (static_cast<std::underlying_type_t <T>>(lhs) X static_cast<std::underlying_type_t <T>>(rhs)); } 
#define ENUM_FLAGS(T) \
enum class T; \
inline T operator ~ (T t) { return (T) (~static_cast<std::underlying_type_t <T>>(t)); } \
ENUM_FLAG_OPERATOR(T,|) \
ENUM_FLAG_OPERATOR(T,^) \
ENUM_FLAG_OPERATOR(T,&) \
enum class T

使用简单

ENUM_FLAGS(Fish)
{
    OneFish,
    TwoFish,
    RedFish,
    BlueFish
};

而且,正如他们所说,证明在布丁中:

ENUM_FLAGS(Hands)
{
    NoHands = 0,
    OneHand = 1 << 0,
    TwoHands = 1 << 1,
    LeftHand = 1 << 2,
    RightHand = 1 << 3
};

Hands hands = Hands::OneHand | Hands::TwoHands;
if ( ( (hands & ~Hands::OneHand) ^ (Hands::TwoHands) ) == Hands::NoHands)
{
    std::cout << "Look ma, no hands!" << std::endl;
}

可以随意定义任何合适的单个运算符,但在我偏颇的观点中,C / C ++用于与低级概念和流进行接口,您可以将这些按位运算符从我冷酷无情的手中撬出来。然后我会召唤所有邪恶的宏和可翻转的咒语来与您战斗,以保留它们。


2
如果您如此讨厌宏,为什么不使用正确的 C ++构造并编写一些模板运算符而不是宏呢?可以说,模板方法是更好的方法,因为您可以使用std::enable_ifwith std::is_enum将自由的运算符重载限制为仅使用枚举类型。我还添加了比较运算符(使用std::underlying_type)和逻辑非运算符,以进一步缩小差距而又不会丢失强类型。我无法企及的唯一的事情是隐式转换为BOOL,但flags != 0!flags是够我用。
monkey0506

1

通常,您将定义一组整数值,这些整数值与单个位设置的二进制数相对应,然后将它们加在一起。这是C程序员通常这样做的方式。

因此,您将拥有(使用bitshift运算符设置值,例如1 << 2与二进制100相同)

#define ENUM_1 1
#define ENUM_2 1 << 1
#define ENUM_3 1 << 2

等等

在C ++中,您有更多选择,请定义一个新类型,而不是int(使用typedef)并类似地设置值;或定义布尔位域向量。后两个非常节省空间,在处理标志时更有意义。位域的优点是可以进行类型检查(因此可以进行智能感知)。

我想(显然是主观的)说,C ++程序员应该为您的问题使用位域,但是我倾向于在C ++程序中看到C程序经常使用的#define方法。

我认为位域最接近C#的枚举,为什么C#试图将枚举重载为位域类型很奇怪-枚举实际上应该是“单选”类型。


11
以这种方式使用在C ++中的宏是坏
BЈовић

3
C ++ 14允许您定义二进制文字(例如0b0100),因此1 << n格式有点过时了。
Rob K

也许你的意思是位集合,而不是位字段。
豪尔赫·贝隆

1

下面的enum-flags的简短示例看起来很像C#。

在我看来,关于这种方法:更少的代码,更少的错误,更好的代码。

#indlude "enum_flags.h"

ENUM_FLAGS(foo_t)
enum class foo_t
    {
     none           = 0x00
    ,a              = 0x01
    ,b              = 0x02
    };

ENUM_FLAGS(foo2_t)
enum class foo2_t
    {
     none           = 0x00
    ,d              = 0x01
    ,e              = 0x02
    };  

int _tmain(int argc, _TCHAR* argv[])
    {
    if(flags(foo_t::a & foo_t::b)) {};
    // if(flags(foo2_t::d & foo_t::b)) {};  // Type safety test - won't compile if uncomment
    };

ENUM_FLAGS(T)是在enum_flags.h中定义的宏(少于100行,可以不受限制地自由使用)。


1
enum_flags.h文件与您的问题的第一个修订版中的文件相同吗?如果是,则可以使用修订版URL进行引用:http
gnat 2013年

+1看起来不错,干净。我将在我们的SDK项目中进行尝试。
Garet Claborn 2014年

1
@GaretClaborn这就是我所说的干净的:paste.ubuntu.com/23883996
sehe 2017年

1
当然,错过了::type那里。固定:paste.ubuntu.com/23884820
sehe

@sehe嘿,模板代码不应该清晰易懂。这是什么巫术?好的....这是段开通使用洛尔
杰拉尔德Claborn

0

还有另一种为猫皮的方法:

除了重载位运算符之外,至少有些人可能更喜欢仅添加4个内衬来帮助您规避作用域枚举的讨厌限制:

#include <cstdio>
#include <cstdint>
#include <type_traits>

enum class Foo : uint16_t { A = 0, B = 1, C = 2 };

// ut_cast() casts the enum to its underlying type.
template <typename T>
inline auto ut_cast(T x) -> std::enable_if_t<std::is_enum_v<T>,std::underlying_type_t<T>>
{
    return static_cast<std::underlying_type_t<T> >(x);
}

int main(int argc, const char*argv[])
{
   Foo foo{static_cast<Foo>(ut_cast(Foo::B) | ut_cast(Foo::C))};
   Foo x{ Foo::C };
   if(0 != (ut_cast(x) & ut_cast(foo)) )
       puts("works!");
    else 
        puts("DID NOT WORK - ARGHH");
   return 0;
}

当然,您ut_cast()每次都必须键入事物,但从正面static_cast<>()看,与隐式类型转换或operator uint16_t()事物相比,这产生的可读性更高,与使用do的意义相同。

老实说,Foo在上面的代码中使用type会带来危险:

在其他地方,某人可能会对变量进行切换,foo而并不期望它拥有多个值...

因此,用乱七八糟的代码ut_cast()来提醒读者某些事情正在发生。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.