Explore C++20 Bit Flag with Designated Initializer and Concepts

The bit flag or bit mask is a recurring topic in the C++ community. Since enum class has been added, the attempt to create a more type-safe and/or easier-to-use bit flag has never ended. It never succeeded either, as all solutions were flawed in some ways that they've never beaten the straightforward enum. Some define bitwise-operators for enum class but use additional macros or specializations, while still requiring manually defined power-of-2 values. Some others create a wrapper class, but lost the capability to express the flag with combination of enum literals. With several features coming to C++20, I'd like to explore another possible design.

struct AnimationFlag
{
    bool FullMatch   : 1 = false; //initialization is optional here
    bool PlayForever : 1 = false;
    bool NonLooping  : 1 = false;
};

// usage
AnimationFlag f { .FullMatch = true, .NonLooping = true };

From memory layout point of view, a flag value is rather a (mis)interpretation of an offset in the bit field. Even though bit field always existed in C++, now the difference is that with Designated initializer, we can actually define it with a single, clean statement. In the example above, .FullMatch and .NonLooping are initialized to 1 whereas all others to 0. If you want to absolutely make sure value is always 0-initialized, Bit field member in-class initialization is helpful albeit more verbose.

Now, is it good enough for production? Considering several use cases -

  • Composing a mask from both positive and negative flags: f = a | b or f = all & ~c
  • Determining if a flag is on or off: (f & a) != 0
  • Manipulating flags: f |= (a | b) or f ^= c

In our case, determining or manipulating a single flag is trivial. However, it's still missing the expression support - combining flags with bitwise operators (preferably 0-cost and supports constexpr) is an important part of the design. We can overload operator |, &, ^ for a generic type T and only allow "our types" by using SFINAE, but it's a really dangerous decision due to the potential conflict with other generic overloads in 3rd-party libraries, and the huge impact on compilation time.

In C++20 we are finally going to have Concepts, although at this point it's unclear how much build time advantage it provides.

//credit https://stackoverflow.com/questions/39768517/structured-bindings-width
struct filler { template< typename type > operator type (); };

template< typename aggregate, typename index_sequence = std::index_sequence<>, typename = void >
struct aggregate_arity
        : index_sequence
{
};

template< typename aggregate, std::size_t ...indices >
struct aggregate_arity< aggregate, std::index_sequence< indices... >, std::void_t< decltype(aggregate{(indices, std::declval< filler >())..., std::declval< filler >()}) > >
    : aggregate_arity< aggregate, std::index_sequence< indices..., sizeof...(indices) > >
{
};

template< typename aggregate >
constexpr std::size_t get_aggregate_arity()
{
    return aggregate_arity< std::remove_reference_t< std::remove_cv_t< aggregate > > >::size();
}

template<typename T>
struct always_false : std::false_type {};

struct flag{};

template<typename T> requires std::is_base_of_v<flag, T>
constexpr T operator& (T a, T b)
{
    constexpr std::size_t arity = get_aggregate_arity<T>();
    if constexpr (arity == 2/*empty base takes 1*/)
    {
        auto [a1] = a;
        auto [b1] = b;
        return T {{}, a1&b1};
    }
    else if constexpr (arity == 3)
    {
        auto [a1,a2] = a;
        auto [b1,b2] = b;
        return T {{}, !!(a1&b1), !!(a2&b2)};
    }
    else if constexpr (arity == 4)
    {
        auto [a1,a2,a3] = a;
        auto [b1,b2,b3] = b;
        return T {{}, !!(a1&b1), !!(a2&b2), !!(a3&b3)};
    }
    else if constexpr (arity == 5)
    {
        auto [a1,a2,a3,a4] = a;
        auto [b1,b2,b3,b4] = b;
        return T {{}, !!(a1&b1), !!(a2&b2), !!(a3&b3), !!(a4&b4)};
    }
    //all the way up to 32 or 64 ... 
    else
    {
        static_assert(always_false<T>::value, "not supported");
        return T{};
    }
}

In order to support arbitrary length and constexpr for the bit flag, we can't reinterpret_cast the underlying memory or use type punning (UB!). That's where structured binding kicks in - although the implementation is tedious due to the lack of variadic structured binding.

struct AnimationFlag : flag
{
    bool FullMatch : 1; // = 0 is optional
    bool PlayForever : 1;
    bool NonLooping : 1;
};

constexpr AnimationFlag f = {.FullMatch = true, .PlayForever = true};
constexpr AnimationFlag f2 = f & ~AnimationFlag{.FullMatch = true};
static_assert(!f2.FullMatch && f2.PlayForever && !f2.NonLooping);

I'd say this is equivalent or better than macro / const / enum based bit flags because

  • It doesn't require defining and maintaining power-of-two values
  • It's type safe
  • It supports any number of flags

What do you think?

Explore C++20 Bit Flag with Designated Initializer and Concepts
Share this