下面是我尝试提供一种将枚举和位集组合在一起的类型安全方式的尝试。结果类打算用作一组标志,其中每个标志都是“用户定义的枚举”的成员。

用户不得为枚举成员定义任何显式值。

下面是“库文件”(1个标头),后面是提供用法示例的测试程序。

#include <iostream>
#include <bitset>
#include <cassert>

/**
 * FlagSet implements a bitset usable with `enum` and `enum class`.
 *
 * It provide a typesafe interface for manipulating the bitset. This helps
 * prevents mistake as the various operator and function will refuse a
 * parameter that doesn't match the expected enum type.
 *
 * A flagset supports one user-defined enumeration. The number of flags
 * (ie the member of the user enumeration) is not limited, as the underlying
 * bitset (std::bitset) can have an arbitrary large size.
 * 
 * REQUIREMENTS:
 *      * This code source required C++14 to compile.
 *      * The user enumeration shall not explicitely set any value.
 *      * The last enumeration member shall be: "__SENTINEL__"
 *
 */
template<typename T>
struct FlagSet
{
  FlagSet() = default;
  FlagSet(const FlagSet &o) : bitset(o.bitset) {}

  FlagSet &operator|=(const T &val)
  {
    set_true(val);
    return *this;
  }

  FlagSet &operator&=(const T &val)
  {
    bool tmp = bitset[static_cast<utype>(val)] ? true : false;
    bitset.reset();
    bitset[static_cast<utype>(val)] = tmp;
    return *this;
  }

  FlagSet &operator|=(const FlagSet &o)
  {
    bitset |= o.bitset;
    return *this;
  }

  FlagSet &operator&=(const FlagSet &o)
  {
    bitset &= o.bitset;
    return *this;
  }

  FlagSet &operator=(const FlagSet &o)
  {
    bitset = o.bitset;
  }

  /**
   * Return a bitset containing the result of the
   * bitwise AND between *this and val.
   *
   * The resulting bitset can contain at most 1 bit.
   */
  FlagSet operator&(const T&val)
  {
    FlagSet ret(*this);
    ret &= val;

    assert(ret.bitset.count() <= 1);
    return ret;
  }

  /**
   * Perform a AND binary operation between *this and
   * `val` and return the result as a copy.
   */
  FlagSet operator&(const FlagSet &val)
  {
    FlagSet ret(*this);
    ret.bitset &= val.bitset;

    return ret;
  }

  /**
   * Return a bitset containing the result of the
   * bitwise OR between *this and val.
   *
   * The resulting bitset contains at least 1 bit.
   */
  FlagSet operator|(const T&val)
  {
    FlagSet ret(*this);
    ret |= val;

    assert(ret.bitset.count() >= 1);
    return ret;
  }

  /**
   * Perform a OR binary operation between *this and
   * `val` and return the result as a copy.
   */
  FlagSet operator|(const FlagSet &val)
  {
    FlagSet ret(*this);
    ret.bitset |= val.bitset;

    return ret;
  }

  FlagSet operator~()
  {
    FlagSet cp(*this);
    cp.bitset.flip();

    return cp;
  }

  /**
   * The bitset evaluates to true if any bit is set.
   */
  operator bool() const
  {
    return bitset.any();
  }

  /**
   * Below are the method from std::bitset that we expose.
   */

  bool operator==(const FlagSet &o) const
  {
    return bitset == o.bitset;
  }

  std::size_t size() const
  {
    return bitset.size();
  }

  std::size_t count() const
  {
    return bitset.count();
  }

  FlagSet &set()
  {
    bitset.set();
        return *this;
  }

  FlagSet &reset()
  {
    bitset.reset();
        return *this;
  }

  FlagSet &flip()
  {
    bitset.flip();
    return *this;
  }

  FlagSet &set(const T &val, bool value = true)
  {
    bitset.set(static_cast<utype>(val), value);
        return *this;
  }

  FlagSet &reset(const T&val)
  {
    bitset.reset(static_cast<utype>(val));
    return *this;
  }

  FlagSet &flip(const T &val)
  {
    bitset.flip(static_cast<utype>(val));
    return *this;
  }

  bool operator[](const T&val)
  {
    return bitset[static_cast<utype>(val)];
  }

  /**
   * Overload for std::ostream
   */
  friend std::ostream& operator<<(std::ostream& stream, const FlagSet& me)
  {
    return stream << me.bitset;
  }

private:
  using utype = std::underlying_type_t<T>;
  std::bitset<static_cast<utype>(T::__SENTINEL__)> bitset;

  void set_true(const T&val)
  {
    bitset[static_cast<utype>(val)] = 1;
  }

};

/**
 * Provide a free operator allowing to combine two enumeration
 * member into a FlagSet.
 */
template<typename T>
std::enable_if_t<std::is_enum<T>::value, FlagSet<T>>
operator|(const T &lhs, const T &rhs)
{
  FlagSet<T> bs;
  bs |= lhs;
  bs |= rhs;

  return bs;
}


示例文件:

#include "flagset.hpp"

/**
 * Some random enum to use in tests.
 */ 
enum class Options : uint64_t
{
  FULLSCREEN,
    INVERT_MOUSE,
    BLA,
    RED_BACKGROUND,
    RED_FOREGROUND,
    __SENTINEL__
    };


int test_AND()
{
  FlagSet<Options> red(Options::RED_FOREGROUND | Options::RED_BACKGROUND);

  auto ret = red & Options::RED_BACKGROUND;
  assert(ret);
  assert(ret.count() == 1);

  ret = red & Options::RED_FOREGROUND;
  assert(ret);
  assert(ret.count() == 1);

  ret = red & (Options::RED_FOREGROUND | Options::RED_BACKGROUND);
  assert(ret);
  assert(ret.count() == 2);

  ret = ~red & Options::RED_BACKGROUND;
  assert(ret == false);
  assert(ret.count() == 0);
}

int test_OR()
{
  FlagSet<Options> red;
  red |= Options::RED_FOREGROUND | Options::RED_BACKGROUND;
  assert(red.count() == 2);

  FlagSet<Options> opt;
  opt |= (Options::FULLSCREEN | Options::BLA);

  // FULLSCREEN and BLA match, so this evaluates to true.
  assert(opt & (Options::FULLSCREEN | Options::BLA | Options::RED_FOREGROUND | Options::RED_BACKGROUND));

  // Ensure that a group of flag is set
  FlagSet<Options> expected;
  expected |= (Options::FULLSCREEN);
  assert((opt & expected) == expected);

  assert((opt & (Options::RED_FOREGROUND | Options::RED_BACKGROUND)) == false);
  assert((opt & red) == false);

  assert(!(opt & Options::INVERT_MOUSE));
  opt |= ~red;
  assert(opt & Options::INVERT_MOUSE);
}

int test_set_reset()
{
  FlagSet<Options> opt;

  assert(opt.count() == 0);
  opt.set();
  assert(opt.count() == opt.size() && opt.size() == 5);
  opt.reset();
  assert(opt.count() == 0);

  opt.set(Options::BLA);
  assert(opt.count() == 1 && opt[Options::BLA]);
  opt.set(Options::BLA, false);
  assert(opt.count() == 0);  
}

int test_type_safety()
{
  // The following will not compile.
  FlagSet<Options> bs;

  // bs & 42;
  // bs &= 42;
  // bs |= 42;
  // bs | 42;
}

int main()
{
  auto t = std::bitset<4>();
  assert((int)Options::FULLSCREEN == 0);
  test_AND();
  test_OR();
  test_set_reset();
} 


我对几乎所有反馈都感兴趣:从实现细节到API以及命名(FlagSet是我能想到的最好的名字)。 br />

评论

很好,但是当我第一次上课时,那个__SENTINEL__让我失望了。在我看到用法示例之前,我以为您依赖于编译器的一些晦涩之处。使用双下划线的名称仅用于编译器/标准库名称:stackoverflow.com/a/228797/1198654

#1 楼

这看起来不错。我会提出以下建议。

首先,您仅在一个地方使用set_value。这是一个非常不必要的功能,只需满足以下条件即可:

FlagSet &operator|=(const T &val)
{
    bitset.set(static_cast<utype>(val));
    return *this;
}


其次,对于&=来说,表达式expr ? true : false是一个反模式,可以仅用expr代替。在这种情况下,您甚至不需要它,因为bitset提供了test。整个事情可能变成:

FlagSet &operator&=(const T &val)
{
    bool tmp = bitset.test(static_cast<utype>(val));
    bitset.reset();
    bitset.set(static_cast<utype>(val), tmp);
    return *this;
}


第三,您缺少return:真的,因为这些都是琐碎的,所以很漂亮:

FlagSet &operator=(const FlagSet &o)
{
    bitset = o.bitset;
    return *this; // <== 
}


甚至不指定它们。

第四,更喜欢使default显式显示:

FlagSet(const FlagSet& ) = default;
FlagSet& operator=(const FlagSet& ) = default;


这将防止编译诸如operator bool之类的怪异表达式。 >最后,FlagSet{} + 4是C ++标准中的保留字。您可以改为要求将其命名为其他名称,例如__SENTINEL__LAST_VALUE等。

评论


\ $ \ begingroup \ $
我还没有测试过,但是可以通过使用sizeof(std :: underlying_type_t )* 8来减轻__SENTINEL__的使用。
\ $ \ endgroup \ $
–马克·英格拉姆
18/12/18在11:29

\ $ \ begingroup \ $
@MarkIngram或sizeof(std :: underlying_type_t )* CHAR_BIT只是为了安全起见? :)
\ $ \ endgroup \ $
–轨道轻赛
18/12/18在11:30

\ $ \ begingroup \ $
它也缺少一些const(例如operator |)
\ $ \ endgroup \ $
–轨道轻赛
18/12/18在11:51

\ $ \ begingroup \ $
最后,我建议添加从T到FlagSet 的非显式转换,以便您可以轻松地从单个标志进行构造。不知道为什么我没有把这些东西重新回答
\ $ \ endgroup \ $
–轨道轻赛
18/12/18在12:31

\ $ \ begingroup \ $
如此处所建议,使用std :: bitset :: max()>是摆脱__SENTINEL__的一种方法。但是,这将具有诸如破坏set()的明显缺点。
\ $ \ endgroup \ $
– mrts
19-10-4在21:03

#2 楼

谢谢@Xaqq,这真的很棒! >
 #include "flag_set.hpp"

enum class Options : uint8_t {
    FULLSCREEN,
    INVERT_MOUSE,
    FLASH,
    RED_BACKGROUND,
    RED_FOREGROUND,
    _
};

int main()
{
    flag_set<Options> red(Options::RED_FOREGROUND | Options::RED_BACKGROUND);

    if (red & Options::RED_BACKGROUND)
        cout << "Red background activated";
}
 


请注意,我选择_作为前哨,因为它很明显并且不太可能与实际枚举值。另外,我觉得遵循ISO标准库下划线样式命名是很自然的,因为这是类似于标准库类的通用实用程序类。资料库。 @Xaqq,如果您愿意,我可以为您提供资源库,因为您是合法的作者。

#3 楼

请注意,

 // Operator that combines two enumeration values into a flag_set.
template <typename T>
std::enable_if_t<std::is_enum<T>::value, flag_set<T>> operator|(const T& lhs, const T& rhs)
 


operator|添加到所有枚举,包括那些没有预期的特殊哨兵。

例如Qt的Q_DECLARE_METATYPE()宏。

可以用

 template <typename T, typename = void>
struct is_enum_that_contains_sentinel :
    std::false_type {};

template <typename T>
struct is_enum_that_contains_sentinel<T, decltype(static_cast<void>(T::_))> :
    std::is_enum<T> {};

// Operator that combines two enumeration values into a flag_set only if the
// enumeration contains the sentinel `_`.
template <typename T>
std::enable_if_t<is_enum_that_contains_sentinel<T>::value, flag_set<T>>
    operator|(const T& lhs, const T& rhs)
{
    ...
 


修复。 >(基于JonathanMüller的代码,可在GitHub上找到。)