Given different values represented by an enum, the requirement is to pass a list of those values to another system. It’s not mandatory to use an enum, I’ve chosen it just as a use case. The list of values can be a vector, an array, a bitset, or any other container or utility that can hold multiple values. Depending on the context, there are advantages and disadvantages over one container or another.
Options
A vector is very easy to use but uses dynamic memory allocation. In an embedded context, the use of dynamic memory can be restricted.
An array uses preallocated memory and has a fixed number of elements, which could be the maximum number of values in the enum. If you want to send fewer elements than the maximum, you must have a convention to let the other system know how many you are sending. This is because you will always send a fixed number of elements (the maximum). You can choose a special value that indicates an element in the array is not of interest. Or you can place the values you want to send starting with the first position in the array, and pass along another value that says how many elements you are sending.
For a bitset, you must know the number of bits used. And you have a general semantic of manipulating values. If these aspects are convenient, a bitset can be an option.
The most simple option I can think of is a bitwise representation on an unsigned, fixed-width integer type (eg: uint32_t). It can give you the smallest memory space to represent multiple values. If you need this list of values in a very small scope, like a small function where you set the bits on a variable which you pass to the other system, it might be enough. If you pass this variable in larger scopes of the project, it’s a matter of time until someone does not know what that variable holds (there is no semantic). And they might confuse it for a variable that holds a single value, not a representation of multiple ones. Then, operations like equality might work by coincidence in some cases, other cases being runtime bugs.
For all of the above and other options not mentioned, you need to obtain the underlying value of the enum. An unscoped enum is implicitly converted to the numeric type you use for the option you choose. From a scoped enum you have to explicitly get the value.
A tailored abstraction
I’m aiming for a container with a small size, no dynamic memory, easy to pass around, and with a very good semantic so that it’s totally clear what it holds.
A class with clear operations, such as the STD bitset has: set values, add new ones besides the already existing ones, remove values, verify if values are in the container. And it manages by itself how to obtain the underlying values from the enum.
template <typename Enum> struct container { void set(Enum); void add(Enum); void remove(Enum); void reset(); template <typename... Ts> bool none(Enum, Ts...); template <typename... Ts> bool some(Enum, Ts...); };
I define an enum and pass it as a template argument to the container. Then I can simply use the container’s API to manipulate the list of enum values.
template <typename Enum> enum class Values : std::uint32_t { a = 1, b = 2, c = 3, d = 4, }; container<Values> list; list.set(Values::a); list.none(Values::b, Values::c); list.reset();
Storage
The actual list of values will be a class member with the underlying type of the enum. Because I’ll be using bitwise operations, I will restrict the type to be unsigned. Some bitwise operations are defined for signed types, too, but I’m choosing to be stricter.
A requirement of the enum is that, for simplicity, 0 (zero) will not be used as a value. It represents “no elements in the list”. Of course, a solution can be found for this if it’s a must.
Implementation
Summing up where I want to get to: A class with an expressive API to abstract bitwise operations on a class member representing the list of values. I’ve chosen the C++17 standard because I wanted to use, for the first time, the fold expression. This container can be easily implemented using an older standard.
Construct
I’m naming this container enum_list
. First, I’m making sure, at compile-time, that the enum requirements are respected, and I’m constructing the container:
-
- the default constructer for an empty list
- and a constructor that takes an existing “list” of values represented as bits.
template <typename Enum> class enum_list { public: static_assert(std::is_enum_v<Enum>, "an enum is required"); using Type = typename std::underlying_type_t<Enum>; static_assert(std::is_unsigned_v<Type>, "the underlying type of the enum must be unsigned"); enum_list() = default; constexpr explicit enum_list(Type values) : values_{values} {}; private: Type values_{}; };
Write
This container should be flexible enough to cover all possible cases. I’m describing only one write operation, others being similar or simpler; they are all present at the end of the article.
template <typename Enum> class enum_list { public: // ... template <typename... Ts> void set(Enum v, Ts... vs) noexcept { values_ = (get_underlying_value(v) | ... | get_underlying_value(vs)); } // ... private: Type values_{}; static constexpr Type get_underlying_value(Enum v) noexcept { return 1U << static_cast<Type>(v); }
The set
method replaces the existing list with a new one formed with the given arguments. It accepts one or multiple enum values, converts them to a representation (powers of 2) that can be used with the bitwise operations for the storage member, and adds them to the storage. I unpack the parameter pack using the fold expression.
Because the enum can be a scoped one, I’m casting the value to its underlying type.
Other methods add
values to the existing list, remove
existing values from the list, load
a new list or reset
it. They all use bitwise operations or assignments.
Read
When you want to obtain information about the list, you want to get
the list, to see if it’s empty
, or if all/some
of the enum values passed as arguments are in the list.
The read methods are similar to the write ones. An important difference is that I declared them constexpr so they can be used at compile-time.
template <typename Enum> class enum_list { public: // ... template <typename... Ts> constexpr bool all(Enum v, Ts... vs) const noexcept { return ((values_ & get_underlying_value(v)) && ... && all(vs)); } // ...
Better control over the enum values
If your enum values are already powers of 2 because you need so for other contexts, the get_underlying_value
has almost no point. It works, but for large values, it can generate even larger ones that could not fit in the list.
Or you can have large values from the start. Or you could be in any possible case that would not be suited for the current implementation of
get_underlying_value
.
For maximum flexibility of how the values are converted to an appropriate bitwise representation, the enum list accepts a custom converter (and offers a default one). The requirement for this converter is to be a class with a static, constexpr, and noexcept method called convert
which accepts an enum value and returns a bitwise representation with the enum’s underlying type.
struct ShiftConverter { template <typename Enum, typename Type> static constexpr Type convert(const Enum v) noexcept { return 1U << static_cast<Type>(v); } }; template <typename Enum, typename Converter = ShiftConverter> class enum_list { // ... private: // ... static constexpr Type get_underlying_value(Enum v) noexcept { return Converter::template convert<Enum, Type>(v); } };
(Almost) No overhead
Although this container is not a really small class, when it comes to performance it has the chance to respect the zero-overhead principle. It could get close to the equivalent of a variable with the enum’s underlying type. This depends on the compiler, the optimization level, and how you use the API methods. There are cases where a compiler might not be able to inline everything. You should analyze it in your context.
When you need to pass it over to other systems that do not specifically need a reference to it, it can be copied as easily as the enum’s underlying type.
static_assert(sizeof(enum_list<Values>) == sizeof(std::underlying_type_t<Values>)); static_assert(std::is_trivially_copyable_v<enum_list<Values>>);
Other overhead points are, of course, maintenance and compilation. Like any class, it can have bugs, it can require new features and tests. And the compilers need to do some work and optimizations. Although the price for these aspects might be considered low and the benefits high, the end product is not free.
Full implementation
#include <cassert> #include <cstdint> #include <type_traits> enum class Values : std::uint32_t { a = 1, b = 2, c = 3, d = 4, }; enum class BitValues : std::uint32_t { a = 1 << 1, b = 1 << 2, c = 1 << 3, d = 1 << 4, }; struct ShiftConverter { template <typename Enum, typename Type> static constexpr Type convert(const Enum v) noexcept { return 1U << static_cast<Type>(v); } }; struct ValueConverter { template <typename Enum, typename Type> static constexpr Type convert(const Enum v) noexcept { return static_cast<Type>(v); } }; template <typename Enum, typename Converter = ShiftConverter> class enum_list { public: static_assert(std::is_enum_v<Enum>, "an enum is required"); using Type = typename std::underlying_type_t<Enum>; static_assert(std::is_unsigned_v<Type>, "the underlying type of the enum must be unsigned"); enum_list() = default; constexpr explicit enum_list(Type values) : values_{values} {}; template <typename... Ts> void set(Enum v, Ts... vs) noexcept { values_ = (get_underlying_value(v) | ... | get_underlying_value(vs)); } template <typename... Ts> void add(Enum v, Ts... vs) noexcept { values_ |= (get_underlying_value(v) | ... | get_underlying_value(vs)); } template <typename... Ts> void remove(Enum v, Ts... vs) noexcept { values_ &= (~get_underlying_value(v) & ... & ~get_underlying_value(vs)); } void load(Type values) noexcept { values_ = values; } void reset() noexcept { values_ = 0U; } constexpr Type get() const noexcept { return values_; } constexpr bool none() const noexcept { return values_ == 0U; } template <typename... Ts> constexpr bool all(Enum v, Ts... vs) const noexcept { return ((values_ & get_underlying_value(v)) && ... && all(vs)); } template <typename... Ts> constexpr bool some(Enum v, Ts... vs) const noexcept { return ((values_ & get_underlying_value(v)) || ... || some(vs)); } private: Type values_{}; static constexpr Type get_underlying_value(Enum v) noexcept { return Converter::template convert<Enum, Type>(v); } }; int main() { static_assert(sizeof(enum_list<Values>) == sizeof(std::underlying_type_t<Values>)); static_assert(std::is_trivially_copyable_v<enum_list<Values>>); enum_list<Values> list{6U}; assert(list.get() == 6U); assert(list.all(Values::a, Values::b)); assert(!list.all(Values::a, Values::b, Values::c)); assert(list.some(Values::a, Values::b)); assert(!list.some(Values::c)); list.set(Values::d, Values::c); assert(list.get() == 24U); list.add(Values::b, Values::a, Values::a, Values::a, Values::a, Values::a); assert(list.get() == 30U); assert(!list.none()); list.reset(); assert(list.none()); list.add(Values::a, Values::b, Values::c, Values::d); assert(list.get() == 30U); list.remove(Values::a, Values::b); assert(list.get() == 24U); list.load(30U); assert(list.get() == 30U); assert(list.all(Values::a, Values::b, Values::c, Values::d)); // Compile-time constexpr enum_list<Values> const_list{2U}; static_assert(const_list.get() == 2U); static_assert(!const_list.none()); static_assert(const_list.all(Values::a)); constexpr enum_list<Values> const_empty_list{}; static_assert(!const_empty_list.some(Values::a)); // Custom converter constexpr enum_list<BitValues, ValueConverter> value_list{2U}; static_assert(value_list.some(BitValues::a)); }