自动为 enum 类型添加位运算操作符

有时候需要在 C++ 里用 enum (class) 表示 flags,进行基础的 bitwise 运算,而哪怕是支持自动到 underlying integer 转换的 traditional enum,也需要额外的 cast 才能实现,所以在需要的时候为 enum 添加位运算支持还是有一定市场的。

单独为每个需要的 enum 提供相关重载可以实现细粒度的控制,并且必要的时候可以加上 range-check 的校验。

然而这里有两个问题:

  1. 大部分情况下不需要 range-check 这种检查,因为通常不会在接口这一层暴露
  2. 如果一个 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;
}