自动为 enum 类型添加位运算操作符
有时候需要在 C++ 里用 enum (class)
表示 flags,进行基础的 bitwise 运算,而哪怕是支持自动到 underlying integer 转换的 traditional enum,也需要额外的 cast 才能实现,所以在需要的时候为 enum
添加位运算支持还是有一定市场的。
单独为每个需要的 enum
提供相关重载可以实现细粒度的控制,并且必要的时候可以加上 range-check 的校验。
然而这里有两个问题:
- 大部分情况下不需要 range-check 这种检查,因为通常不会在接口这一层暴露
- 如果一个 module 里会用到两个以上的 enum-based-flags,那么单独实现会很蛋疼
所以一个解决方案是,提供基于函数模板的重载,并且利用 ADL 将这部分重载锁在一个具体的 namespace 内,例如马上(C++ 20)就要被废弃的 rel_ops
。
同时,我们希望这部分重载只对 enum / enum class
有效,其他类型不进行应用。可以通过 SFINAE 实现这点。
利用快下班的时间做了一个基本的实现:
// Casts an enum value into an equivalent integer.
template<typename E>
constexpr auto enum_cast(E e) noexcept
{
return static_cast<std::underlying_type_t<E>>(e);
}
namespace enum_ops {
template<typename E>
constexpr std::enable_if_t<std::is_enum<E>::value, E> operator|(E lhs, E rhs) noexcept
{
return E(enum_cast(lhs) | enum_cast(rhs));
}
template<typename E>
constexpr std::enable_if_t<std::is_enum<E>::value, E&> operator|=(E& lhs, E rhs) noexcept
{
lhs = E(enum_cast(lhs) | enum_cast(rhs));
return lhs;
}
template<typename E>
constexpr std::enable_if_t<std::is_enum<E>::value, E> operator&(E lhs, E rhs) noexcept
{
return E(enum_cast(lhs) & enum_cast(rhs));
}
template<typename E>
constexpr std::enable_if_t<std::is_enum<E>::value, E&> operator&=(E& lhs, E rhs) noexcept
{
lhs = E(enum_cast(lhs) & enum_cast(rhs));
return lhs;
}
template<typename E>
constexpr std::enable_if_t<std::is_enum<E>::value, E> operator^(E lhs, E rhs) noexcept
{
return E(enum_cast(lhs) ^ enum_cast(rhs));
}
template<typename E>
constexpr std::enable_if_t<std::is_enum<E>::value, E&> operator^=(E& lhs, E rhs) noexcept
{
lhs = E(enum_cast(lhs) ^ enum_cast(rhs));
return lhs;
}
template<typename E>
constexpr std::enable_if_t<std::is_enum<E>::value, E> operator~(E op) noexcept
{
return E(~enum_cast(op));
}
} // namespace enum_ops
测试代码:
enum class ChangeLevel : unsigned int {
None = 0,
Local = 1 << 0,
Server = 1 << 1,
All = Local | Server
};
TEST(EnumOps, General)
{
using namespace enum_ops;
ChangeLevel level = ChangeLevel::None;
level |= ChangeLevel::Local;
level |= ChangeLevel::Server;
EXPECT_TRUE(level == ChangeLevel::All);
EXPECT_TRUE(enum_cast(level & ChangeLevel::Local) != 0);
EXPECT_TRUE(enum_cast(level & ChangeLevel::Server) != 0);
}
TEST(EnumOps, NonEnumType)
{
using namespace enum_ops;
unsigned int l = 1;
unsigned int r = 1 << 1;
auto rv = l | r;
EXPECT_EQ(3, rv);
// can't compile
//std::string s1 = "hello";
//std::string s2 = "world";
//s1 | s2;
}