18
\$\begingroup\$

Below is my attempt at providing a type-safe way to combine enumeration and bitset together. The resulting class is intended to be use as a set of flags, where each flag is a member of the "user-defined enumeration".

The user must not define any explicit value to the enumeration member.

Below is the "library file" (1 header), followed by a test program providing the usage example.

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

Example file:

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

I am interested in pretty much all feedback: from implementation details, to API as well as naming (FlagSet being the best name I could come up with).

\$\endgroup\$
  • 3
    \$\begingroup\$ Very nice, but that __SENTINEL__ threw me off when I first spotted in the class. Until I saw the usage example, I thought that you were relying on some obscure corner of your compiler. Names using double underscore are only meant to be used for compiler/standard-library names: stackoverflow.com/a/228797/1198654 \$\endgroup\$ – glampert Jul 8 '15 at 2:56
14
\$\begingroup\$

This looks pretty good. I'd propose the following.

First, you use set_value in exactly one place. It's a pretty unnecessary function, and just this would suffice:

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

Second, for &=, the expression expr ? true : false is an antipattern that can be replaced with just expr. In this case, you don't even need that as bitset provides test. The whole thing can become:

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

Third, you're missing a return:

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

But really, since these are trivial, pretty to default them:

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

Or don't even specify them.

Fourth, prefer to make operator bool explicit:

explicit operator bool() const
{
    return bitset.any();
}

That will prevent weird expressions like FlagSet{} + 4 from compiling.

Lastly, __SENTINEL__ is a reserved word in the C++ Standard. You could instead require it to be named something else, like LAST_VALUE or MAX_VALUE or something.

\$\endgroup\$
  • 1
    \$\begingroup\$ I haven't tested this, but the use of __SENTINEL__ could be alleviated by using sizeof(std::underlying_type_t<T>) * 8 instead. \$\endgroup\$ – Mark Ingram Dec 18 '18 at 11:29
  • 2
    \$\begingroup\$ @MarkIngram Or sizeof(std::underlying_type_t<T>)*CHAR_BIT just to be safe? :) \$\endgroup\$ – Lightness Races in Orbit Dec 18 '18 at 11:30
  • \$\begingroup\$ It's missing a few const as well (e.g. operator|) \$\endgroup\$ – Lightness Races in Orbit Dec 18 '18 at 11:51
  • \$\begingroup\$ Finally, I propose adding a non-explicit conversion from T to FlagSet<T> so you can easily construct from a single flag. Don't know why I'm not putting these things in a fresh answer really \$\endgroup\$ – Lightness Races in Orbit Dec 18 '18 at 12:31
  • 1
    \$\begingroup\$ As proposed here, using std::bitset<std::numeric_limits<utype>::max()> is a way to get rid of __SENTINEL__. However, that will have obvious drawbacks like breaking set(). \$\endgroup\$ – mrts Oct 4 at 21:03
2
\$\begingroup\$

Thanks, @Xaqq, this is really nice!

I published the code in GitHub with amendments from @Barry and @lightness-races-in-orbit and minor modifications, usage is as follows:

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

Note that I chose as sentinel as it stands out clearly and is unlikely to collide with a enum value. This works well with Clang, but breaks with GCC. Also, I felt that it is natural to follow the ISO standard library underscore style naming as this is a general utility class similar to standard library classes.

Please feel free to file further amendments as issues in the GitHub repository. @Xaqq, if you want to, I can yield the repository to you as you are the rightful author.

\$\endgroup\$
2
\$\begingroup\$

Note that

// 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)

adds operator| to all enums, including those that don't have the expected special sentinel.

This breaks e.g. Qt's Q_DECLARE_METATYPE() macro.

This can be fixed with

template <typename Enum>
struct is_flag_set<Enum, decltype(static_cast<void>(Enum::__SENTINEL__))> : std::is_enum<Enum>
{};

template <typename T>
std::enable_if_t<is_flag_set<T>::value, flag_set<T>> operator|(const T& lhs, const T& rhs)

(Based on Jonathan Müller's code.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.