向作用域枚举添加按位运算和布尔值转换——圣诞节探索
Adding bitwise operations and conversion-to-bool to scoped enums - a Christmastide exploration
假设我疯了,决定创造以下怪物:
#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;
}
目标是:
- 有一个作用域枚举,值是
Foo::x
并且类型是 Foo
(对称!)
- 能够对它们进行一些 "bitwise" 运算并获得
Foo
回报
- 能够检查结果是否为零
- 但不要让人们普遍使用枚举作为算术类型
为了好玩,我尽量避免:
- 使用沼泽标准枚举
- 用自由函数代替
IsFlagSet
暂时忽略没有事先&
-或|
-操作就无法进行零度检查的不协调……
我的用户仍然可以 "get" 和 EnumToBoolProxy
(即 proxyThing
),这似乎是一种耻辱。但是,由于无法向 Foo
添加任何成员,而且 operator bool
必须是成员,所以我似乎找不到任何其他方法来解决这个问题。
当然这不是真正的问题,因为他们无法用 EnumToBoolProxy
做很多事情。但它仍然感觉像是抽象泄漏,所以我很好奇:我说这本质上是不可能的是对的吗?没有办法 "pick and choose" 像这样的作用域枚举的不透明度?或者是否有某种方法可以隐藏此代理类型,同时仍用作转换为布尔工具来检查 &
/|
操作的 "result"?你会怎么做?
嗯,这可能不是你想要的,但你说 "hide this proxy type"。所以你可以把它隐藏在下面更可怕的地方。
现在生成的类型是一个隐藏代理的 lambda :)
#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.
}
所以这是另一个解决方案,也许是一个更严肃的解决方案。
它满足您的所有要求,甚至 "avoid using a bog-standard enum"。
除了 Foo 值的类型不是 Foo,而是类似 CRTP-Foo 的东西。
User-API 类似于真正的枚举,但比我的其他答案有一些优势:
- 不需要贪婪或受 SFINAE 保护的运营商。
- 不再有代理 class。
- 是constexpr
。
- 无需调用&
或|
即可直接进行零校验。
#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);
}
假设我疯了,决定创造以下怪物:
#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;
}
目标是:
- 有一个作用域枚举,值是
Foo::x
并且类型是Foo
(对称!) - 能够对它们进行一些 "bitwise" 运算并获得
Foo
回报 - 能够检查结果是否为零
- 但不要让人们普遍使用枚举作为算术类型
为了好玩,我尽量避免:
- 使用沼泽标准枚举
- 用自由函数代替
IsFlagSet
暂时忽略没有事先&
-或|
-操作就无法进行零度检查的不协调……
我的用户仍然可以 "get" 和 EnumToBoolProxy
(即 proxyThing
),这似乎是一种耻辱。但是,由于无法向 Foo
添加任何成员,而且 operator bool
必须是成员,所以我似乎找不到任何其他方法来解决这个问题。
当然这不是真正的问题,因为他们无法用 EnumToBoolProxy
做很多事情。但它仍然感觉像是抽象泄漏,所以我很好奇:我说这本质上是不可能的是对的吗?没有办法 "pick and choose" 像这样的作用域枚举的不透明度?或者是否有某种方法可以隐藏此代理类型,同时仍用作转换为布尔工具来检查 &
/|
操作的 "result"?你会怎么做?
嗯,这可能不是你想要的,但你说 "hide this proxy type"。所以你可以把它隐藏在下面更可怕的地方。 现在生成的类型是一个隐藏代理的 lambda :)
#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.
}
所以这是另一个解决方案,也许是一个更严肃的解决方案。
它满足您的所有要求,甚至 "avoid using a bog-standard enum"。 除了 Foo 值的类型不是 Foo,而是类似 CRTP-Foo 的东西。
User-API 类似于真正的枚举,但比我的其他答案有一些优势:
- 不需要贪婪或受 SFINAE 保护的运营商。
- 不再有代理 class。
- 是constexpr
。
- 无需调用&
或|
即可直接进行零校验。
#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);
}