Adding bitwise operations and conversion-to-bool to scoped enums – a Christmastide exploration

  • A+
Category:Languages

Let's say that I'm crazy and decided to create the following monstrosity:

#include <type_traits> #include <iostream>  // Utility proxy type - convertible back to E but also permits bool conversion // for use in conditions. //  //  e.g. //   Foo f = Foo::Bar & Foo::Baz; //   if (f & Foo::Baz) { /* ... */ } //  template <typename E, typename = std::enable_if_t<std::is_enum_v<E>, void>> struct EnumToBoolProxy {        operator E() const     {         return _val;     }      explicit operator bool()     {         using UT = std::underlying_type_t<E>;         return static_cast<UT>(_val) != 0;     }  private:     const E _val;      EnumToBoolProxy(const E val) : _val(val) {}      friend EnumToBoolProxy operator&(const E, const E);     friend EnumToBoolProxy operator|(const E, const E); };   enum class Foo {     Bar = 1, Baz = 2, Boi = 4 };  EnumToBoolProxy<Foo> operator&(const Foo lhs, const Foo rhs) {     using UT = std::underlying_type_t<Foo>;     return static_cast<Foo>(static_cast<UT>(lhs) & static_cast<UT>(rhs)); }  EnumToBoolProxy<Foo> operator|(const Foo lhs, const Foo rhs) {     using UT = std::underlying_type_t<Foo>;     return static_cast<Foo>(static_cast<UT>(lhs) | static_cast<UT>(rhs)); }   int main() {     // Good     if ((Foo::Bar | Foo::Baz) & Foo::Baz)         std::cout << "Yay/n";      // Fine     const bool isFlagSet((Foo::Bar | Foo::Baz) & Foo::Baz);     std::cout << isFlagSet << '/n';      // Meh     auto proxyThing = (Foo::Bar | Foo::Baz) & Foo::Baz; } 

The goal is to:

  1. Have a scoped enum such that values are Foo::x and are of type Foo (symmetry!)
  2. Be able to do some "bitwise" arithmetic on them and get a Foo back
  3. Be able to check the result for zero-ness
  4. But not let people generally use the enum as an arithmetic type

For fun, I'm trying to avoid:

  1. Using a bog-standard enum
  2. Doing this instead with free functions IsFlagSet

Ignoring for a second the incongruity of not being able to do the zero-ness check without a prior &- or |-operation…

It seems like a shame that my users can still "get" a EnumToBoolProxy (i.e. proxyThing). But, since it is not possible to add any members to Foo, and since operator bool must be a member, I can't seem to find any other way to go about this.

Granted that's not a real problem as they can't do much with the EnumToBoolProxy. But it still feels like an abstraction leak, so I'm curious: am I right in saying that this is just inherently impossible? That there's no way to "pick and choose" a scoped-enum's opacity like this? Or is there some way to hide this proxy type while still using as a conversion-to-bool facility to check the "result" of the &/| operations? How would you do it?

 


So here is another solution, perhaps a more serious one.

It fits all your requirements even the "avoid using a bog-standard enum". Except that the type of a Foo-value is not Foo, but a CRTP-Foo-ish thing.

The User-API is similar to a real enum, but with some advantages over my other answer: - don't need greedy or SFINAE-protected operators. - No proxy class anymore. - It is constexpr. - Zero-check can be done directly without the need to call & or | before.

#include <type_traits> #include <iostream>  // Utility proxy type - convertible back to E but also permits bool conversion // for use in conditions. // //  e.g. //   Foo f = Foo::Bar & Foo::Baz; //   if (f & Foo::Baz) { /* ... */ } //  template<unsigned x, typename Base> struct EnumVal :  std::integral_constant<unsigned, x> {    };  struct Foo;  template<unsigned x> using FooVal = EnumVal<x, Foo>;  struct Foo {      static constexpr FooVal<1> Bar;     static constexpr FooVal<2> Baz;     static constexpr FooVal<4> Boi; };   template<unsigned lhs, unsigned rhs> EnumVal<(lhs & rhs), Foo> constexpr operator&( EnumVal<lhs, Foo> ,  EnumVal<rhs, Foo> ) {     return {}; }   template<unsigned lhs, unsigned rhs> EnumVal<(lhs | rhs), Foo> constexpr operator|( EnumVal<lhs, Foo> ,  EnumVal<rhs, Foo> ) {     return {}; }   template<typename T> constexpr void print_type(T) {     static_assert(std::is_same_v<T, void>, "YOU WANTED TO READ THIS TYPE!"); }  int main() {    // Not an arithmetic type :)    static_assert(!std::is_arithmetic_v<decltype(Foo::Bar)>);      static_assert(Foo::Bar);     static_assert(!(Foo::Bar & Foo::Baz));      // Good     if ((Foo::Bar | Foo::Baz) & Foo::Baz)         std::cout << "Yay/n";      // Fine     const bool isFlagSet = (Foo::Bar | Foo::Baz) & Foo::Baz;     std::cout << isFlagSet << '/n';      // Finally really not a proxy thing anymore!     auto proxyThing = (Foo::Bar | Foo::Baz) & Foo::Baz;     // print_type(proxyThing);  } 

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: