Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385104

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

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?

Upvotes: 11

Views: 379

Answers (2)

florestan
florestan

Reputation: 4655

Well this is probably not what you want, but you said "hide this proxy type". So you could hide it in the following even more monstrosity. Now the resulting type is a lambda hiding your proxy :)

#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) { /* ... */ }
//
auto lam = [](auto e) {

    struct Key {};    

    //template <typename E, typename = std::enable_if_t<std::is_enum_v<E>, void>>
    struct EnumToBoolProxy {
        using E = decltype(e);

        operator E() const {
            return _val;
        }

        explicit operator bool() {
            using UT = std::underlying_type_t<E>;
            return static_cast<UT>(_val) != 0;
        }

        EnumToBoolProxy(const E val, Key) : _val(val) {}


    private:
        const E _val;
    };

    return EnumToBoolProxy(e, Key{});

};

enum class Foo {
    Bar = 1, Baz = 2, Boi = 4
};

auto operator&(const Foo lhs, const Foo rhs) {
    using UT = std::underlying_type_t<Foo>;
    return lam(static_cast<Foo>(static_cast<UT>(lhs) & static_cast<UT>(rhs)));
}

template<typename T, std::enable_if_t<std::is_same_v<T, decltype(lam)>>>
auto operator&(T lhs, const Foo rhs) {
    using UT = std::underlying_type_t<Foo>;
    return lam(static_cast<Foo>(static_cast<UT>(lhs) & static_cast<UT>(rhs)));
}


auto operator|(const Foo lhs, const Foo rhs) {
    using UT = std::underlying_type_t<Foo>;
    return lam(static_cast<Foo>(static_cast<UT>(lhs) | static_cast<UT>(rhs)));
}



int main() {
    lam(Foo::Bar);
    // 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';

    // OK, still a proxy thing
    auto proxyThing = (Foo::Bar | Foo::Baz) & Foo::Baz;

    using Proxy = decltype(proxyThing);

    //Proxy proxy2(Foo::Bar); // Does not work anymore.

}

Upvotes: 6

florestan
florestan

Reputation: 4655

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

}

Upvotes: 6

Related Questions