如何在C++中使用枚举作为标志?

Ton*_*ony 171 c++ enums

enums作为标志处理可以在C#中通过[Flags]属性很好地工作,但是在C++中执行此操作的最佳方法是什么?

例如,我想写:

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};

seahawk.flags = CanFly | EatsFish | Endangered;
Run Code Online (Sandbox Code Playgroud)

但是,我收到关于int/ enum转换的编译器错误.是否有更好的表达方式而不仅仅是直接的铸造?优选地,我不想依赖来自第三方库的构造,例如boost或Qt.

编辑:如答案中所示,我可以通过声明seahawk.flags为避免编译器错误int.但是,我想有一些机制来强制执行类型安全,所以有人不能写seahawk.flags = HasMaximizeButton.

小智 230

"正确"的方法是为枚举定义位运算符,如:

enum AnimalFlags
{
    HasClaws   = 1,
    CanFly     = 2,
    EatsFish   = 4,
    Endangered = 8
};

inline AnimalFlags operator|(AnimalFlags a, AnimalFlags b)
{
    return static_cast<AnimalFlags>(static_cast<int>(a) | static_cast<int>(b));
}
Run Code Online (Sandbox Code Playgroud)

等等其他位运算符.如果枚举范围超出int范围,则根据需要进行修改.

  • ^这个.唯一的问题是如何自动化/模板化操作符定义,因此您不必在每次添加新枚举时不断定义它们. (41认同)
  • @LightnessRacesinOrbit:这不对.枚举类型的域是其基础类型的域 - 只有某些类型的域名被赋予了它.并回答你的问题:成员"`(HasClaws | CanFly)`". (23认同)
  • 另外,即使int值与任何枚举标识符不对应,从任意int转换回枚举类型是否有效? (8认同)
  • 这完全是胡说八道.`AnimalFlags`的哪个成员由表达式'HasClaws |表示 CanFly`?这不是什么`enum`s for.使用整数和常量. (6认同)
  • @Xeo:FWIW,在接下来的几年里,我改变了对枚举域的看法 ^_^ 尽管我仍然没有完全接受将这些用于位掩码常量而不是命名空间中的一堆常量,范围枚举让我们足够接近使其值得平衡。 (4认同)
  • @Zaffy,也许您可​​以重新定义“ | =”运算符? (3认同)
  • @MarcusJ:将值限制为2的幂允许您将枚举用作位标志.因此,如果你得到3,你就知道它是'HasClaws`(= 1)和`CanFly`(= 2).相反,如果您直接指定值1到4并且得到3,它可能是单个"EatsFish",或者再次是"HasClaws"和"CanFly"的组合.如果枚举仅表示独占状态,则连续值很好,但标志组合需要值为位独占. (3认同)
  • @Xeo:仅仅因为它是合法的并不意味着它是一个好主意。枚举用于枚举。这就是为什么它们被称为枚举。遗憾的是,C++ 允许这样做(“可以定义一个枚举,其值不是由任何枚举器定义的”),但我怀疑这是为了与现有的、遗留的、糟糕的 C 软件兼容。 (2认同)
  • 还要添加“&amp;”、“&amp;=”和“~”运算符。以及相关的“&amp;=”和“*”(乘法)。地面绑定由 `(animal &amp;~CanFly)` 选择。失去飞行是“animal &amp;=~CanFly”。定义动物是否可以在不改变其他属性的情况下飞行(对于`bool getFlightAbility()`)是`animal = (animal &amp;~CanFly) | (CanFly*getFlightAbility())`。真的值得让人头疼吗? (2认同)
  • (为了澄清@Eljay的观点,从C++17开始,将整数/浮点/枚举值“V”转换为任何枚举类型“E”的结果,其中“E”的基础类型未指定,并且“ V` 超出了 `E` 的范围,是未定义的行为(在 C++17 之前未指定)。这里,`E` 的范围由可以容纳所有 `E` 的最小位域定义。 s 枚举器;例如,给定 `enum E { A = 1, B = 2, C = 4, };`,`E` 的范围是 `0..7`。这可以防止任意赋值,但明确设计为不影响“bitflag”枚举。) (2认同)
  • 这似乎是个坏主意。最终有人会为此编写一个“详尽的”“switch”,并且编译器不会警告您您尚未检查“HasClaws |”这一事实。CanFly`等 (2认同)

Wou*_*rvD 119

注意(也有点偏离主题):使用位移可以完成另一种制作唯一标志的方法.我,我自己,发现这更容易阅读.

enum Flags
{
    A = 1 << 0, // binary 0001
    B = 1 << 1, // binary 0010
    C = 1 << 2, // binary 0100
    D = 1 << 3, // binary 1000
};
Run Code Online (Sandbox Code Playgroud)

它可以将值保持为int,因此在大多数情况下,32个标志清楚地反映在移位量中.

  • 它是.我只是发现0,1,2,3等更容易阅读. (45认同)
  • @Katu • 标准允许在最终枚举中使用多余的逗号。我不喜欢它,但我已经知道 Stroustrup 会告诉我什么......“你不喜欢它?好吧,随意创建你自己的语言。我做到了。” (4认同)
  • 没有提到十六进制?亵渎! (3认同)
  • @迈克尔,这是真的!在枚举中,您通常为 BLAH_NONE 保留 0。:-) 感谢您打破记忆! (3认同)
  • 你能否删除最后一个逗号(3,)并在}之后添加冒号以使代码易于复制和粘贴?谢谢 (2认同)

use*_*594 51

对于像我这样的懒人,这里是复制和粘贴的模板化解决方案:

template<class T> inline T operator~ (T a) { return (T)~(int)a; }
template<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }
template<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }
template<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }
template<class T> inline T& operator|= (T& a, T b) { return (T&)((int&)a |= (int)b); }
template<class T> inline T& operator&= (T& a, T b) { return (T&)((int&)a &= (int)b); }
template<class T> inline T& operator^= (T& a, T b) { return (T&)((int&)a ^= (int)b); }
Run Code Online (Sandbox Code Playgroud)

  • +1 Laziness是程序员的三大优点之一:http://threevirtues.com/ (18认同)
  • 不要使用此代码.它为任何课程的错误操作打开了大门.此外,代码使用旧样式转换,不会通过GCC严格编译http://shitalshah.com/p/how-to-enable-and-use-gcc-strict-mode-compilation/. (14认同)
  • 这是一个非常好的解决方案,请注意它将为任何类型快乐地提供按位运算.我正在使用类似的东西,但添加了一些特征,用于识别我希望它应用的类型以及一点启用魔法. (8认同)
  • 请注意,这应该使用“std::underlying_type”而不是“int”,除非保证它始终与不指定基础类型的“enum class”一起使用(“enum class”默认为“int”)底层类型,普通的“enum”默认为“无论有效,但它可能是“int”,除非枚举器对于“int”来说太大”)。 (4认同)
  • 您可以将 SFINAE `enable_if` 与 `is_enum` 一起使用,以仅在枚举上启用这些运算符。 (3认同)

Bri*_*ndy 42

seahawk.flags变量是什么类型的?

在标准C++中,枚举不是类型安全的.它们实际上是整数.

AnimalFlags不应该是变量的类型,你的变量应该是int,错误就会消失.

像其他人建议的那样放置十六进制值是不需要的,它没有任何区别.

默认情况下,枚举值为int类型.所以你肯定可以按位或组合它们并将它们放在一起并将结果存储在int中.

枚举类型是int的受限子集,其值是其枚举值之一.因此,当您在该范围之外创建一些新值时,如果不转换为枚举类型的变量,则无法分配它.

如果您愿意,也可以更改枚举值类型,但这个问题没有意义.

编辑:海报说他们关心类型安全,他们不希望int类型中不应存在的值.

但是将AnimalFlags范围之外的值放在AnimalFlags类型的变量中是不安全的.

虽然在int类型中有一种安全的方法可以检查超出范围的值...

int iFlags = HasClaws | CanFly;
//InvalidAnimalFlagMaxValue-1 gives you a value of all the bits 
// smaller than itself set to 1
//This check makes sure that no other bits are set.
assert(iFlags & ~(InvalidAnimalFlagMaxValue-1) == 0);

enum AnimalFlags {
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8,

    // put new enum values above here
    InvalidAnimalFlagMaxValue = 16
};
Run Code Online (Sandbox Code Playgroud)

以上并不会阻止您从具有值1,2,4或8的不同枚举中放入无效标志.

如果你想要绝对类型安全,那么你可以简单地创建一个std :: set并将每个标志存储在那里.它不是空间有效的,但它是类型安全的,并提供与bitflag int相同的能力.

C++ 0x note:强类型枚举

在C++ 0x中,您最终可以拥有类型安全的枚举值....

enum class AnimalFlags {
    CanFly = 2,
    HasClaws = 4
};

if(CanFly == 2) { }//Compiling error
Run Code Online (Sandbox Code Playgroud)

  • 枚举值不是整数,但它们很容易转换为整数.'HasClaws |的类型 CanFly`是一些整数类型,但是`HasClaws`的类型是`AnimalFlags`,而不是整数类型. (4认同)
  • @Scott:值得注意的是,C ++标准以这种方式定义了枚举实例的有效值范围。“对于emin是最小的枚举数且emax是最大的枚举,该枚举的值是bmin到bmax范围内的值,定义如下:设K为2的补码表示,0为1的补码表示。 bmax是大于或等于“ max(| emin | − K,| emax |)”且等于“(1u &lt;&lt; M)-1”的最小值,其中“ M”为一个非负整数。” (2认同)

Sim*_*ier 39

请注意,如果您在Windows环境中工作,则DEFINE_ENUM_FLAG_OPERATORS在winnt.h中定义了一个为您完成工作的宏.所以在这种情况下,你可以这样做:

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};
DEFINE_ENUM_FLAG_OPERATORS(AnimalFlags)

seahawk.flags = CanFly | EatsFish | Endangered;
Run Code Online (Sandbox Code Playgroud)

  • 值得一提的是,“DEFINE_ENUM_FLAG_OPERATORS”宏也适用于 C++11“enum class”宏,为类型化标志提供了完整的解决方案。 (2认同)

uli*_*ess 25

我发现eidolon目前接受的答案太危险了.编译器的优化器可能会对枚举中的可能值进行假设,并且您可能会使用无效值返回垃圾.通常没有人想要在标志枚举中定义所有可能的排列.

正如Brian R. Bondy在下面所述,如果您正在使用C++ 11(每个人都应该这样,它就是那么好),您现在可以更轻松地使用C++ 11 enum class:

enum class ObjectType : uint32_t
{
    ANIMAL = (1 << 0),
    VEGETABLE = (1 << 1),
    MINERAL = (1 << 2)
};


constexpr enum ObjectType operator |( const enum ObjectType selfValue, const enum ObjectType inValue )
{
    return (enum ObjectType)(uint32_t(selfValue) | uint32_t(inValue));
}

// ... add more operators here. 
Run Code Online (Sandbox Code Playgroud)

这通过指定枚举的类型来确保稳定的大小和值范围,通过使用禁止自动向下注入枚举等enum class,并且用于constexpr确保运算符的代码被内联并因此与常规数字一样快.

对于那些坚持使用11前C++方言的人

如果我遇到了不支持C++ 11的编译器,我会在一个类中包装一个int类型,然后只允许使用按位运算符和该枚举中的类型来设置它的值:

template<class ENUM,class UNDERLYING=typename std::underlying_type<ENUM>::type>
class SafeEnum
{
public:
    SafeEnum() : mFlags(0) {}
    SafeEnum( ENUM singleFlag ) : mFlags(singleFlag) {}
    SafeEnum( const SafeEnum& original ) : mFlags(original.mFlags) {}

    SafeEnum&   operator |=( ENUM addValue )    { mFlags |= addValue; return *this; }
    SafeEnum    operator |( ENUM addValue )     { SafeEnum  result(*this); result |= addValue; return result; }
    SafeEnum&   operator &=( ENUM maskValue )   { mFlags &= maskValue; return *this; }
    SafeEnum    operator &( ENUM maskValue )    { SafeEnum  result(*this); result &= maskValue; return result; }
    SafeEnum    operator ~()    { SafeEnum  result(*this); result.mFlags = ~result.mFlags; return result; }
    explicit operator bool()                    { return mFlags != 0; }

protected:
    UNDERLYING  mFlags;
};
Run Code Online (Sandbox Code Playgroud)

您可以将其定义为常规枚举+ typedef:

enum TFlags_
{
    EFlagsNone  = 0,
    EFlagOne    = (1 << 0),
    EFlagTwo    = (1 << 1),
    EFlagThree  = (1 << 2),
    EFlagFour   = (1 << 3)
};

typedef SafeEnum<enum TFlags_>  TFlags;
Run Code Online (Sandbox Code Playgroud)

用法也类似:

TFlags      myFlags;

myFlags |= EFlagTwo;
myFlags |= EFlagThree;

if( myFlags & EFlagTwo )
    std::cout << "flag 2 is set" << std::endl;
if( (myFlags & EFlagFour) == EFlagsNone )
    std::cout << "flag 4 is not set" << std::endl;
Run Code Online (Sandbox Code Playgroud)

您还可以enum foo : type使用第二个模板参数覆盖二进制稳定枚举的基础类型(如C++ 11 ),即typedef SafeEnum<enum TFlags_,uint8_t> TFlags;.

operator bool使用C++ 11的explicit关键字标记了覆盖,以防止它导致int转换,因为这些可能导致标记集在写出时最终折叠为0或1.如果你不能使用C++ 11,请保留该重载并重写示例用法中的第一个条件(myFlags & EFlagTwo) == EFlagTwo.


sor*_*oru 18

最简单的方法来做到这一点,如图在这里,使用标准库类的bitset.

要以类型安全的方式模拟C#功能,您必须在bitset周围编写一个模板包装器,将int参数替换为作为模板的类型参数给出的枚举.就像是:

    template <class T, int N>
class FlagSet
{

    bitset<N> bits;

    FlagSet(T enumVal)
    {
        bits.set(enumVal);
    }

    // etc.
};

enum MyFlags
{
    FLAG_ONE,
    FLAG_TWO
};

FlagSet<MyFlags, 2> myFlag;
Run Code Online (Sandbox Code Playgroud)

  • 请查看此代码以获取更完整的代码:http://codereview.stackexchange.com/questions/96146/c-flagset-typesafe-usage-of-enumeration-as-bitset-bitmask (4认同)

Tre*_*vor 11

在我看来,到目前为止,没有一个答案是理想的.为了理想我会期望解决方案:

  1. 支持==,!=,=,&,&=,|,|=~在传统意义上运营商(即,a & b)
  2. 是类型安全的,即不允许分配非枚举值,如文字或整数类型(枚举值的按位组合除外)或允许将枚举变量分配给整数类型
  3. 允许表达式如 if (a & b)...
  4. 不需要邪恶的宏,实现特定的功能或其他黑客

到目前为止,大多数解决方案都落在第2点或第3点.在我看来,WebDancer是关闭但在第3点失败,需要为每个枚举重复.

我提出的解决方案是WebDancer的通用版本,它也解决了第3点:

#include <cstdint>
#include <type_traits>

template<typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
class auto_bool
{
    T val_;
public:
    constexpr auto_bool(T val) : val_(val) {}
    constexpr operator T() const { return val_; }
    constexpr explicit operator bool() const
    {
        return static_cast<std::underlying_type_t<T>>(val_) != 0;
    }
};

template <typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
constexpr auto_bool<T> operator&(T lhs, T rhs)
{
    return static_cast<T>(
        static_cast<typename std::underlying_type<T>::type>(lhs) &
        static_cast<typename std::underlying_type<T>::type>(rhs));
}

template <typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
constexpr T operator|(T lhs, T rhs)
{
    return static_cast<T>(
        static_cast<typename std::underlying_type<T>::type>(lhs) |
        static_cast<typename std::underlying_type<T>::type>(rhs));
}

enum class AnimalFlags : uint8_t 
{
    HasClaws = 1,
    CanFly = 2,
    EatsFish = 4,
    Endangered = 8
};

enum class PlantFlags : uint8_t
{
    HasLeaves = 1,
    HasFlowers = 2,
    HasFruit = 4,
    HasThorns = 8
};

int main()
{
    AnimalFlags seahawk = AnimalFlags::CanFly;        // Compiles, as expected
    AnimalFlags lion = AnimalFlags::HasClaws;         // Compiles, as expected
    PlantFlags rose = PlantFlags::HasFlowers;         // Compiles, as expected
//  rose = 1;                                         // Won't compile, as expected
    if (seahawk != lion) {}                           // Compiles, as expected
//  if (seahawk == rose) {}                           // Won't compile, as expected
//  seahawk = PlantFlags::HasThorns;                  // Won't compile, as expected
    seahawk = seahawk | AnimalFlags::EatsFish;        // Compiles, as expected
    lion = AnimalFlags::HasClaws |                    // Compiles, as expected
           AnimalFlags::Endangered;
//  int eagle = AnimalFlags::CanFly |                 // Won't compile, as expected
//              AnimalFlags::HasClaws;
//  int has_claws = seahawk & AnimalFlags::CanFly;    // Won't compile, as expected
    if (seahawk & AnimalFlags::CanFly) {}             // Compiles, as expected
    seahawk = seahawk & AnimalFlags::CanFly;          // Compiles, as expected

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这会创建必要运算符的重载,但使用SFINAE将它们限制为枚举类型.请注意,为了简洁起见,我没有定义所有的运算符,但唯一不同的是运算符&.该运营商目前正在全球(即适用于所有枚举类型),但这可能通过将超载的命名空间(我做什么),或通过增加额外的SFINAE条件被降低或者(可能使用特定的底层类型,或专门设立的类型别名).这underlying_type_t是一个C++ 14的功能,但它似乎得到很好的支持,很容易用简单的C++ 11模拟template<typename T> using underlying_type_t = underlying_type<T>::type;


小智 7

我使用以下宏:

#define ENUM_FLAG_OPERATORS(T)                                                                                                                                            \
    inline T operator~ (T a) { return static_cast<T>( ~static_cast<std::underlying_type<T>::type>(a) ); }                                                                       \
    inline T operator| (T a, T b) { return static_cast<T>( static_cast<std::underlying_type<T>::type>(a) | static_cast<std::underlying_type<T>::type>(b) ); }                   \
    inline T operator& (T a, T b) { return static_cast<T>( static_cast<std::underlying_type<T>::type>(a) & static_cast<std::underlying_type<T>::type>(b) ); }                   \
    inline T operator^ (T a, T b) { return static_cast<T>( static_cast<std::underlying_type<T>::type>(a) ^ static_cast<std::underlying_type<T>::type>(b) ); }                   \
    inline T& operator|= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) |= static_cast<std::underlying_type<T>::type>(b) ); }   \
    inline T& operator&= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) &= static_cast<std::underlying_type<T>::type>(b) ); }   \
    inline T& operator^= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) ^= static_cast<std::underlying_type<T>::type>(b) ); }
Run Code Online (Sandbox Code Playgroud)

它类似于上面提到的那些,但有几个改进:

  • 它是类型安全的(它不假设底层类型是 an int
  • 它不需要手动指定底层类型(与@LunarEclipse 的答案相反)

它确实需要包含 type_traits:

#include <type_traits>
Run Code Online (Sandbox Code Playgroud)


Oma*_*air 6

我发现自己问了同样的问题,并提出了一个基于C++ 11的通用解决方案,类似于soru:

template <typename TENUM>
class FlagSet {

private:
    using TUNDER = typename std::underlying_type<TENUM>::type;
    std::bitset<std::numeric_limits<TUNDER>::max()> m_flags;

public:
    FlagSet() = default;

    template <typename... ARGS>
    FlagSet(TENUM f, ARGS... args) : FlagSet(args...)
    {   
        set(f);
    }   
    FlagSet& set(TENUM f)
    {   
        m_flags.set(static_cast<TUNDER>(f));
        return *this;
    }   
    bool test(TENUM f)
    {   
        return m_flags.test(static_cast<TUNDER>(f));
    }   
    FlagSet& operator|=(TENUM f)
    {   
        return set(f);
    }   
};
Run Code Online (Sandbox Code Playgroud)

界面可以改善味道.然后就可以这样使用:

FlagSet<Flags> flags{Flags::FLAG_A, Flags::FLAG_C};
flags |= Flags::FLAG_D;
Run Code Online (Sandbox Code Playgroud)

  • 除了使用numeric_limits之外,代码几乎相同.我想这是一种类型安全的枚举类的常用方法.我认为使用numeric_limits比在每个枚举的末尾放置一个SENTINEL更好. (5认同)
  • 查看此以获得更好,更完整的代码:http://codereview.stackexchange.com/questions/96146/c-flagset-typesafe-usage-of-enumeration-as-bitset-bitmask (2认同)

Web*_*cer 6

C++标准明确地讨论了这一点,请参见"17.5.2.1.3位掩码类型"一节:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3485.pdf

鉴于这个"模板"你得到:

enum AnimalFlags : unsigned int
{
    HasClaws = 1,
    CanFly = 2,
    EatsFish = 4,
    Endangered = 8
};

constexpr AnimalFlags operator|(AnimalFlags X, AnimalFlags Y) {
    return static_cast<AnimalFlags>(
        static_cast<unsigned int>(X) | static_cast<unsigned int>(Y));
}

AnimalFlags& operator|=(AnimalFlags& X, AnimalFlags Y) {
    X = X | Y; return X;
}
Run Code Online (Sandbox Code Playgroud)

和其他运营商类似.另请注意"constexpr",如果您希望编译器能够执行运算符编译时,则需要它.

如果您正在使用C++/CLI并希望能够分配给refum类的枚举成员,则需要使用跟踪引用:

AnimalFlags% operator|=(AnimalFlags% X, AnimalFlags Y) {
    X = X | Y; return X;
}
Run Code Online (Sandbox Code Playgroud)

注意:此示例不完整,请参阅"17.5.2.1.3位掩码类型"部分以获取完整的运算符集.


Fra*_*sco 5

如果您的编译器还不支持强类型枚举,您可以查看c ++源代码中的以下文章:

从摘要:

本文介绍了解决位操作限制问题的解决方案,
只允许安全合法的操作,并将所有无效位操作转换为编译时错误.最重要的是,位操作的语法保持不变,并且不需要修改使用位的代码,除非可能修复尚未检测到的错误.


vSz*_*kel 5

只有语法糖。没有额外的元数据。

namespace UserRole // grupy
{ 
    constexpr uint8_t dea = 1;
    constexpr uint8_t red = 2;
    constexpr uint8_t stu = 4;
    constexpr uint8_t kie = 8;
    constexpr uint8_t adm = 16;
    constexpr uint8_t mas = 32;
}
Run Code Online (Sandbox Code Playgroud)

整数类型的标志运算符才有效。

  • 恕我直言,这是最好的答案。干净、简单、简单的客户端语法。我只会使用“constexpr int”而不是“constexpr uint8_t”,但概念是相同的。 (2认同)