Compile-time detection of overflow in bitmask

Viewed 91

I'm toying with a general Bitmask class, based on Type-safe Bitmasks in C++, and here is a minimum example highligthning my issue (Compiler Explorer link here):

#include <type_traits>
#include <cstdint>
#include <iostream>

template<class EnumType,
     // Ensure that Bitmask can only be used with enums
     typename = std::enable_if_t<std::is_enum_v<EnumType>>>
class Bitmask
{
   // Type to store bitmask. Should possibly be bigger
   using underlying_type = std::underlying_type_t<EnumType>;

public:
   constexpr Bitmask(EnumType option) : m_mask(bitmaskValue(option))
   {std::cout << "Bitmask " << (int)m_mask << "\n";}

private:
  // m_mask holds the underlying value, e.g. 2 to the power of enum value underlying_type
  m_mask{0};
  static constexpr underlying_type bitmaskValue(EnumType o)
  { return 1 << static_cast<underlying_type>(o); }

  explicit constexpr Bitmask(underlying_type o) : m_mask(o) {}
};


enum class Option : uint8_t
{
   a, b, c, d, e, f, g, h, i
};
enum class Option2 : int
{
   a, b, c, d, e, f, g, h, i
};

int main()
{
    Bitmask<Option> b1{Option::a};
    Bitmask<Option> b2{Option::h};
    Bitmask<Option> b3{Option::i};
    Bitmask<Option2> b4{Option2::i};
}

// Output
Bitmask 1
Bitmask 128
Bitmask 0
Bitmask 256

In short, the Bitmask type works fine as long as the underlying type has more bits than the highest enum value used. If not, the bitmaskValue function will overflow and not give desired result, as shown in output, where b3 gets a value of 0, not 256.

I of course understand that an uint8_t cannot store more than 8 different bits, so what I want is some way to make this a compiler error, either when declaring Bitmask<Option>, or when instatiating a Bitmask with a too high value.

I tried changing the bitmaskValue method to:

   static constexpr underlying_type bitmaskValue(EnumType o) { 
   if constexpr (std::numeric_limits<underlying_type>::digits >= static_cast<underlying_type>(o)) {
       return 1 << static_cast<underlying_type>(o); 
   } else {
       // some error
   }
}

...but then I get the error that 'o' is not a constant expression.

Can anyone helpt me in the correct direction here? For the record, our project is currently using gcc 9.2/c++17, though I hope we can soon upgrade to gcc 11.1/c++20.

Thanks.

2 Answers

As Davis Herring already pointed out you cannot check for the overflow on a non-constant value at runtime (constexpr marked functions may also be executed at runtime). Thus to get the desired functionality one could check for the overflow inside the declaration of a template with the value to test as non-type template parameter.

The basic idea is that with brace initialization with a constant value, the compiler will generate a compilation error when the cast is narrowing, e.g. int8_t{128}; will fail since 128 cannot be represented with int8_t.

template<class EnumType>
class Bitmask
{
   using underlying_type = std::underlying_type_t<EnumType>;

public:
   explicit constexpr Bitmask(underlying_type v) : m_mask{v} {}
   constexpr Bitmask(EnumType option) : m_mask{0x1 << static_cast<underlying_type>(option)}{}

   template <EnumType option> // <- enum value as non-type template parameter
   static constexpr auto make() 
   {
        return Bitmask{underlying_type{0x1 << static_cast<underlying_type>(option)}};
   }
 
   underlying_type m_mask{0};
};
enum class Option : uint8_t
{
   a, b, c, d, e, f, g, h, i
};

Bitmask<Option>::make<Option::f>(); // ok
Bitmask<Option>::make<Option::h>(); // ok
// Bitmask<Option>::make<Option::i>(); // will fail to build

https://godbolt.org/z/117baa4q4

If you control the enum definition as well, the simplest way is just to require an enumerated constant with a fixed name at the end (ie, so it is the largest real value + 1), such as EnumType::MAX_ENUM_VALUE.

Then you can refuse to instantiate if

EnumType::MAX_ENUM_VALUE > std::numeric_limits<underlying_type>::digits + 1

with a static assert, or an enable_if, or whatever. Obviously this will also fail to instantiate if the enumerated constant is missing, so it should be pretty obvious to the user how to fix it.

Note this isn't really a compile-time check for overflow, but for the possibility of overflow. So, it is on the strict side if you only use a subset of your enum's values - but it is consistent with the goal of type safety.

Related