重构作用域枚举按位运算符代码重复

Refactoring scoped enum bitwise operator code duplication

我有几个可以用作按位标志的作用域枚举。我已经为每种类型实现了相同的按位运算符重载,如下所示:

ScopedEnumFoo& operator|=(ScopedEnumFoo& a, const ScopedEnumFoo& b) noexcept {
    using underlying = std::underlying_type_t<ScopedEnumFoo>;
    auto underlying_a = static_cast<underlying>(a);
    auto underlying_b = static_cast<underlying>(b);
    a = static_cast<ScopedEnumFoo>(underlying_a | underlying_b);
    return a;
}

ScopedEnumFoo operator|(ScopedEnumFoo a, const ScopedEnumFoo& b) noexcept {
    a |= b;
    return a;
}

ScopedEnumFoo& operator&=(ScopedEnumFoo& a, const ScopedEnumFoo& b) noexcept {
    using underlying = std::underlying_type_t<ScopedEnumFoo>;
    auto underlying_a = static_cast<underlying>(a);
    auto underlying_b = static_cast<underlying>(b);
    a = static_cast<ScopedEnumFoo>(underlying_a & underlying_b);
    return a;
}

ScopedEnumFoo operator&(ScopedEnumFoo a, const ScopedEnumFoo& b) noexcept {
    a &= b;
    return a;
}

除了作用域枚举的类型之外,代码对于需要用作标志类型的每种类型都是相同的。这会导致代码质量检查员发出嘶嘶声,说我已经重复了十几次(或更多次)代码。

我将如何对代码进行“去重”?有可能吗?

正如杰森·特纳 recently 所说:

'I will not copy-paste code' is the single most important thing you can do to write good code...

当您有相同的函数在不同类型上运行时,这就是模板生成的内容,因此第一步是创建模板:

template <class E>
E& operator|=(E& a, const E& b) noexcept {
    using underlying = std::underlying_type_t<E>;
    auto underlying_a = static_cast<underlying>(a);
    auto underlying_b = static_cast<underlying>(b);
    a = static_cast<E>(underlying_a | underlying_b);
    return a;
}

这个问题是它很乐意接受 任何类型 并且会导致麻烦并以通常意想不到的方式干扰代码的其他部分。我强烈反对这个版本,即使运算符在命名空间后面。

所以它需要限制只需要需要的类型。我将使用概念,因为它们更具表现力。对于 C++20 之前的版本,很容易转换为经典的 SFINAE 技术。

一个快速解决方法是只接受枚举:

template <class E>
    requires std::is_enum_v<E>
E& operator|=(E& a, const E& b) noexcept {
    using underlying = std::underlying_type_t<E>;
    auto underlying_a = static_cast<underlying>(a);
    auto underlying_b = static_cast<underlying>(b);
    a = static_cast<E>(underlying_a | underlying_b);
    return a;
}

这仍然是有问题的,因为它会接受任何枚举类型,甚至是您未定义的枚举类型。要解决这个问题,有一些方法,比如为所有枚举添加一个 sentinel/tag 虚拟枚举值,但我选择的方法是为你的枚举添加一个特征(只需给它一个有意义的名称):

template <class E> struct IsAwesomeEnum: std::false_type {};

template <> struct IsAwesomeEnum<ScopedEnumFoo>  : std::true_type {};
template <> struct IsAwesomeEnum<ScopedEnumBar>  : std::true_type {};

template <class E>
    requires IsAwesomeEnum<E>::value
E& operator|=(E& a, const E& b) noexcept {
    using underlying = std::underlying_type_t<E>;
    auto underlying_a = static_cast<underlying>(a);
    auto underlying_b = static_cast<underlying>(b);
    a = static_cast<E>(underlying_a | underlying_b);
    return a;
}

我会更进一步,为它定义一个概念:

template <class E>
concept AwesomeEnum = IsAwesomeEnum<E>::value;

template <AwesomeEnum E>
E& operator|=(E& a, const E& b) noexcept {
    using underlying = std::underlying_type_t<E>;
    auto underlying_a = static_cast<underlying>(a);
    auto underlying_b = static_cast<underlying>(b);
    a = static_cast<E>(underlying_a | underlying_b);
    return a;
}

为了完整起见,这里是一种非概念 SFINAE 方法:

template <class E, class = std::enable_if_t<IsAwesomeEnum<E>::value>>
E& operator|=(E& a, const E& b) noexcept {
    using underlying = std::underlying_type_t<E>;
    auto underlying_a = static_cast<underlying>(a);
    auto underlying_b = static_cast<underlying>(b);
    a = static_cast<E>(underlying_a | underlying_b);
    return a;
}