Reputation: 385104
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:
Foo::x
and are of type Foo
(symmetry!)Foo
backFor fun, I'm trying to avoid:
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
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
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