C++中的类型安全(更安全)位标志?

23

在修订一些旧的C++代码时,我遇到了几个被定义为枚举类型的位标志

enum FooFlags
{
    FooFlag1 = 1 << 0,
    FooFlag2 = 1 << 1,
    FooFlag3 = 1 << 2
    // etc...
};

这并不罕见,但让我感到困扰的是,一旦你开始组合标志,就会失去类型信息。
int flags = FooFlag1 | FooFlag2;   // We've lost the information that this is a set of flags relating to *Foo*

在stackoverflow上搜索发现,我不是唯一受到这个问题困扰的 之一

一种替代方法是将标志声明为#defines或const integrals,因此位运算不会转换类型(可能)。但这样做的问题是它允许我们的位设置与不相关的标志混合在一起,通过int或其他枚举。

我熟悉std::bitsetboost::dynamic_bitset,但它们都不适用于解决我的问题。我寻求的是类似于 C#的FlagsAttribute 的东西。

我的问题是,除了我自己的解决方案外,还有什么其他的解决方法可以实现更多类型安全性的位标志集合?

我将在下面发布我的解决方案。

3个回答

21

您可以为枚举类型重载运算符,并返回正确类型的结果。

inline FooFlags operator|(FooFlags a, FooFlags b) {
  return static_cast<FooFlags>(+a | +b);
}

需要注意的是,为了在理论上保证安全,您应该手动声明最高可能值,以便枚举类型的范围可以捕获所有组合。

  • 事实上这并不必要:枚举的范围总是能够捕捉到所有可能的组合,因为枚举范围内最大的正数值总是(2^N)-1,其中N是能够表示最高枚举值的位数。这个值的所有位都为1。

需要注意的是,为了在理论上保证安全,您应该手动声明最高可能值,以便枚举类型的范围可以捕获所有组合。

实际上这并不必要:枚举的范围总是能够捕捉到所有可能的组合,因为枚举范围内最大的正数值总是(2^N)-1,其中N是能够表示最高枚举值的位数。这个值的所有位都为1。

2
@sgolodetz,它将进入无限递归(通过调用正在定义的operator|)。 - Johannes Schaub - litb
2
@Johannes Schaub:所以你正在使用加号作为隐式转换为整数的方式。为什么不明确地使用static_cast<>()呢? - Martin York
2
@Martin因为我不知道枚举的基础类型。根据枚举,它可能会溢出longunsigned long。请参阅https://dev59.com/b3VD5IYBdhLWcg3wI3-L#3182557。 - Johannes Schaub - litb
2
@Martin 如果你愿意的话,你也可以写成 ((a + 0) | (b + 0)) - Johannes Schaub - litb
3
可能是我在现实情况下看到和使用“+”的唯一时刻。很酷。 - GManNickG
显示剩余3条评论

14

这是我的解决方案,使用了当前版本的VS2010允许的c++0x元素:

#include <iostream>
#include <numeric>
#include <string>

#include <initializer_list>

template <typename enumT>
class FlagSet
{
    public:

        typedef enumT                     enum_type;
        typedef decltype(enumT()|enumT()) store_type;

        // Default constructor (all 0s)
        FlagSet() : FlagSet(store_type(0))
        {

        }

        // Initializer list constructor
        FlagSet(const std::initializer_list<enum_type>& initList)
        {
            // This line didn't work in the initializer list like I thought it would.  It seems to dislike the use of the lambda.  Forbidden, or a compiler bug?
            flags_ = std::accumulate(initList.begin(), initList.end(), store_type(0), [](enum_type x, enum_type y) { return x | y; })
        }

        // Value constructor
        explicit FlagSet(store_type value) : flags_(value)
        {

        }

        // Explicit conversion operator
        operator store_type() const
        {
            return flags_;
        }

        operator std::string() const
        {
            return to_string();
        }

        bool operator [] (enum_type flag) const
        {
            return test(flag);
        }

        std::string to_string() const
        {
            std::string str(size(), '0');

            for(size_t x = 0; x < size(); ++x)
            {
                str[size()-x-1] = (flags_ & (1<<x) ? '1' : '0');
            }

            return str;
        }

        FlagSet& set()
        {
            flags_ = ~store_type(0);
            return *this;
        }

        FlagSet& set(enum_type flag, bool val = true)
        {
            flags_ = (val ? (flags_|flag) : (flags_&~flag));
            return *this;
        }

        FlagSet& reset()
        {
            flags_ = store_type(0);
            return *this;
        }

        FlagSet& reset(enum_type flag)
        {
            flags_ &= ~flag;
            return *this;
        }

        FlagSet& flip()
        {
            flags_ = ~flags_;
            return *this;
        }

        FlagSet& flip(enum_type flag)
        {
            flags_ ^= flag;
            return *this;
        }

        size_t count() const
        {
            // http://www-graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan

            store_type bits = flags_;
            size_t total = 0;
            for (; bits != 0; ++total)
            {
                bits &= bits - 1; // clear the least significant bit set
            }
            return total;
        }

        /*constexpr*/ size_t size() const   // constexpr not supported in vs2010 yet
        {
            return sizeof(enum_type)*8;
        }

        bool test(enum_type flag) const
        {
            return (flags_ & flag) > 0;
        }

        bool any() const
        {
            return flags_ > 0;
        }

        bool none() const
        {
            return flags == 0;
        }

    private:

        store_type flags_;

};

template<typename enumT>
FlagSet<enumT> operator & (const FlagSet<enumT>& lhs, const FlagSet<enumT>& rhs)
{
    return FlagSet<enumT>(FlagSet<enumT>::store_type(lhs) & FlagSet<enumT>::store_type(rhs));
}

template<typename enumT>
FlagSet<enumT> operator | (const FlagSet<enumT>& lhs, const FlagSet<enumT>& rhs)
{
    return FlagSet<enumT>(FlagSet<enumT>::store_type(lhs) | FlagSet<enumT>::store_type(rhs));
}

template<typename enumT>
FlagSet<enumT> operator ^ (const FlagSet<enumT>& lhs, const FlagSet<enumT>& rhs)
{
    return FlagSet<enumT>(FlagSet<enumT>::store_type(lhs) ^ FlagSet<enumT>::store_type(rhs));
}

template <class charT, class traits, typename enumT>
std::basic_ostream<charT, traits> & operator << (std::basic_ostream<charT, traits>& os, const FlagSet<enumT>& flagSet)
{
    return os << flagSet.to_string();
}

这个接口的模型是基于 std::bitset,我的目标是忠于C++类型安全和最小(如果有的话)开销的理念。我欢迎对我的实现提出任何反馈意见。

以下是一个最简示例:

#include <iostream>

enum KeyMod
{
    Alt     = 1 << 0,  // 1
    Shift   = 1 << 1,  // 2
    Control = 1 << 2   // 4
};

void printState(const FlagSet<KeyMod>& keyMods)
{
    std::cout << "Alt is "     << (keyMods.test(Alt)     ? "set" : "unset") << ".\n";
    std::cout << "Shift is "   << (keyMods.test(Shift)   ? "set" : "unset") << ".\n";
    std::cout << "Control is " << (keyMods.test(Control) ? "set" : "unset") << ".\n";
}

int main(int argc, char* argv[])
{
    FlagSet<KeyMod> keyMods(Shift | Control);

    printState(keyMods);

    keyMods.set(Alt);
    //keyMods.set(24);    // error - an int is not a KeyMod value
    keyMods.set(Shift);
    keyMods.flip(Control);

    printState(keyMods);

    return 0;
}

你的实现有没有使用示例? - Eric
@Eric,我觉得这应该很简单。你具体想要什么? - luke
一个简单的声明枚举E,实例化 FlagSet<E>,然后使用它的例子。当然,我可以自己想出来,但是一个示例会让这个答案更好。 - Eric
2
Qt有一个QFlags<T>类模板,以类型安全的方式包装枚举:http://qt-project.org/doc/qt-5.0/qtcore/qflags.html。它与这个非常相似,但更类似于普通的标志枚举,即它更喜欢重载运算符而不是方法。 - Oberon

8

我想为enum class添加一个C++11版本

FooFlags operator|(FooFlags a, FooFlags b)
{
  typedef std::underlying_type<FooFlags>::type enum_type;
  return static_cast<FooFlags>(static_cast<enum_type>(a) | static_cast<enum_type>(b));
}

如果你的c++11版本支持,我猜这将是一个constexpr的最佳选择。

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