Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 395
Lightness Races in Orbit Avatar asked Dec 17 '18 18:12

Lightness Races in Orbit


1 Answers

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.

}
like image 103
florestan Avatar answered Nov 08 '22 15:11

florestan