在c#中,通过[flags]属性将枚举视为标志,但在c++中实现这一点的最佳方法是什么?

例如,我想写:

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

seahawk.flags = CanFly | EatsFish | Endangered;

然而,我得到编译器错误关于int/enum转换。除了生硬的角色转换,还有更好的表达方式吗?最好,我不想依赖第三方库(如boost或Qt)的构造。

编辑:如答案中所示,我可以通过声明seahawk来避免编译器错误。标记为int。但是,我希望有某种机制来执行类型安全,这样就不能编写seahawk了。flags = HasMaximizeButton。


当前回答

目前还没有语言支持枚举标志,如果它将成为c++标准的一部分,元类可能会固有地添加这个特性。

我的解决方案是创建仅枚举实例化的模板函数,为枚举类使用其底层类型添加类型安全的按位操作支持:

文件:EnumClassBitwise.h

#pragma once
#ifndef _ENUM_CLASS_BITWISE_H_
#define _ENUM_CLASS_BITWISE_H_

#include <type_traits>

//unary ~operator    
template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum& operator~ (Enum& val)
{
    val = static_cast<Enum>(~static_cast<std::underlying_type_t<Enum>>(val));
    return val;
}

// & operator
template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum operator& (Enum lhs, Enum rhs)
{
    return static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) & static_cast<std::underlying_type_t<Enum>>(rhs));
}

// &= operator
template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum operator&= (Enum& lhs, Enum rhs)
{
    lhs = static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) & static_cast<std::underlying_type_t<Enum>>(rhs));
    return lhs;
}

//| operator

template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum operator| (Enum lhs, Enum rhs)
{
    return static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) | static_cast<std::underlying_type_t<Enum>>(rhs));
}
//|= operator

template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum& operator|= (Enum& lhs, Enum rhs)
{
    lhs = static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) | static_cast<std::underlying_type_t<Enum>>(rhs));
    return lhs;
}

#endif // _ENUM_CLASS_BITWISE_H_

为了方便和减少错误,你可能想要包装你的枚举和整数的位标志操作:

文件:BitFlags.h

#pragma once
#ifndef _BIT_FLAGS_H_
#define _BIT_FLAGS_H_

#include "EnumClassBitwise.h"

 template<typename T>
 class BitFlags
 {
 public:

     constexpr inline BitFlags() = default;
     constexpr inline BitFlags(T value) { mValue = value; }
     constexpr inline BitFlags operator| (T rhs) const { return mValue | rhs; }
     constexpr inline BitFlags operator& (T rhs) const { return mValue & rhs; }
     constexpr inline BitFlags operator~ () const { return ~mValue; }
     constexpr inline operator T() const { return mValue; }
     constexpr inline BitFlags& operator|=(T rhs) { mValue |= rhs; return *this; }
     constexpr inline BitFlags& operator&=(T rhs) { mValue &= rhs; return *this; }
     constexpr inline bool test(T rhs) const { return (mValue & rhs) == rhs; }
     constexpr inline void set(T rhs) { mValue |= rhs; }
     constexpr inline void clear(T rhs) { mValue &= ~rhs; }

 private:
     T mValue;
 };
#endif //#define _BIT_FLAGS_H_

可能的用法:

#include <cstdint>
#include <BitFlags.h>
void main()
{
    enum class Options : uint32_t
    { 
          NoOption = 0 << 0
        , Option1  = 1 << 0
        , Option2  = 1 << 1
        , Option3  = 1 << 2
        , Option4  = 1 << 3
    };

    const uint32_t Option1 = 1 << 0;
    const uint32_t Option2 = 1 << 1;
    const uint32_t Option3 = 1 << 2;
    const uint32_t Option4 = 1 << 3;

   //Enum BitFlags
    BitFlags<Options> optionsEnum(Options::NoOption);
    optionsEnum.set(Options::Option1 | Options::Option3);

   //Standard integer BitFlags
    BitFlags<uint32_t> optionsUint32(0);
    optionsUint32.set(Option1 | Option3); 

    return 0;
}

其他回答

海鹰是什么类型的?标志变量?

在标准c++中,枚举不是类型安全的。它们是有效的整数。

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

不需要像其他人建议的那样输入十六进制值。这没什么区别。

enum值默认为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
};

上面所述并不能阻止您从值为1、2、4或8的不同enum中放置无效标志。

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

c++ 0x注意:强类型枚举

在c++ 0x中,您终于可以拥有类型安全的enum值....

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

if(CanFly == 2) { }//Compiling error

“正确”的方法是为枚举定义位操作符,如下所示:

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));
}

等等,其余的位操作符。如果枚举范围超过int range,则根据需要修改。

我想详细说明Uliwitness的回答,为c++ 98修复他的代码,并使用Safe Bool习语,因为在c++ 11以下的c++版本中缺少std::underlying_type<>模板和显式关键字。

我还修改了它,使枚举值可以是连续的,而不需要任何显式的赋值,因此您可以有

enum AnimalFlags_
{
    HasClaws,
    CanFly,
    EatsFish,
    Endangered
};
typedef FlagsEnum<AnimalFlags_> AnimalFlags;

seahawk.flags = AnimalFlags() | CanFly | EatsFish | Endangered;

然后,您可以获得原始标志值

seahawk.flags.value();

这是代码。

template <typename EnumType, typename Underlying = int>
class FlagsEnum
{
    typedef Underlying FlagsEnum::* RestrictedBool;

public:
    FlagsEnum() : m_flags(Underlying()) {}

    FlagsEnum(EnumType singleFlag):
        m_flags(1 << singleFlag)
    {}

    FlagsEnum(const FlagsEnum& original):
        m_flags(original.m_flags)
    {}

    FlagsEnum& operator |=(const FlagsEnum& f) {
        m_flags |= f.m_flags;
        return *this;
    }

    FlagsEnum& operator &=(const FlagsEnum& f) {
        m_flags &= f.m_flags;
        return *this;
    }

    friend FlagsEnum operator |(const FlagsEnum& f1, const FlagsEnum& f2) {
        return FlagsEnum(f1) |= f2;
    }

    friend FlagsEnum operator &(const FlagsEnum& f1, const FlagsEnum& f2) {
        return FlagsEnum(f1) &= f2;
    }

    FlagsEnum operator ~() const {
        FlagsEnum result(*this);
        result.m_flags = ~result.m_flags;
        return result;
    }

    operator RestrictedBool() const {
        return m_flags ? &FlagsEnum::m_flags : 0;
    }

    Underlying value() const {
        return m_flags;
    }

protected:
    Underlying  m_flags;
};

我认为目前接受的答案是eidolon太危险了。编译器的优化器可能会对枚举中可能的值进行假设,您可能会得到带有无效值的垃圾。通常没有人想在标记枚举中定义所有可能的排列。

正如Brian R. Bondy在下面所说的,如果你正在使用c++ 11(每个人都应该这样做,它很好),你现在可以用枚举类更容易地做到这一点:

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. 

这通过为枚举指定类型来确保一个稳定的大小和值范围,通过使用枚举类来抑制枚举自动向下转换为int等,并使用constexpr来确保操作符的代码得到内联,从而与常规数字一样快。

对于那些受困于11年前c++方言的人来说

如果我被一个不支持c++ 11的编译器困住了,我会在一个类中包装一个int类型,然后只允许使用位操作符和该enum的类型来设置它的值:

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;
};

你可以像定义普通的enum + typedef一样定义它:

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

typedef SafeEnum<enum TFlags_>  TFlags;

用法也类似:

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;

你也可以使用第二个模板参数覆盖二进制稳定enum的底层类型(如c++ 11的enum foo: type),即typedef SafeEnum<enum TFlags_,uint8_t> TFlags;。

我用c++ 11的显式关键字标记了bool override操作符,以防止它导致int转换,因为这可能导致一组标志在写入时被折叠成0或1。如果你不能使用c++ 11,那就把这个重载去掉,并重写示例用法中的第一个条件为(myFlags & EFlagTwo) == EFlagTwo。

下面是我的解决方案,不需要任何一堆重载或强制转换:

namespace EFoobar
{
    enum
    {
        FB_A    = 0x1,
        FB_B    = 0x2,
        FB_C    = 0x4,
    };
    typedef long Flags;
}

void Foobar(EFoobar::Flags flags)
{
    if (flags & EFoobar::FB_A)
        // do sth
        ;
    if (flags & EFoobar::FB_B)
        // do sth
        ;
}

void ExampleUsage()
{
    Foobar(EFoobar::FB_A | EFoobar::FB_B);
    EFoobar::Flags otherflags = 0;
    otherflags|= EFoobar::FB_B;
    otherflags&= ~EFoobar::FB_B;
    Foobar(otherflags);
}

我认为这是可以的,因为我们无论如何都会识别(非强类型)枚举和整数。

只是作为一个(较长的)边注,如果你

要使用强类型枚举和 不需要重一点摆弄你的旗帜 性能不是问题

我会想到这个:

#include <set>

enum class EFoobarFlags
{
    FB_A = 1,
    FB_B,
    FB_C,
};

void Foobar(const std::set<EFoobarFlags>& flags)
{
    if (flags.find(EFoobarFlags::FB_A) != flags.end())
        // do sth
        ;
    if (flags.find(EFoobarFlags::FB_B) != flags.end())
        // do sth
        ;
}

void ExampleUsage()
{
    Foobar({EFoobarFlags::FB_A, EFoobarFlags::FB_B});
    std::set<EFoobarFlags> otherflags{};
    otherflags.insert(EFoobarFlags::FB_B);
    otherflags.erase(EFoobarFlags::FB_B);
    Foobar(otherflags);
}

使用c++ 11初始化列表和枚举类。