如何在作用域枚举上重载 |= 运算符?

38

如何在强类型(作用域)enum上重载|=运算符(在C++11,GCC中)?

我想对强类型枚举测试、设置和清除位。为什么要使用强类型?因为我的书说这是个好习惯。但这意味着我必须在各处进行static_cast<int>转换。为了避免这种情况,我重载了|&运算符,但我无法弄清楚如何在枚举类型上重载|=运算符。对于类而言,您只需将运算符定义放在类中,但在枚举类型中,这似乎在语法上行不通。

这是我目前的代码:

enum class NumericType
{
    None                    = 0,

    PadWithZero             = 0x01,
    NegativeSign            = 0x02,
    PositiveSign            = 0x04,
    SpacePrefix             = 0x08
};

inline NumericType operator |(NumericType a, NumericType b)
{
    return static_cast<NumericType>(static_cast<int>(a) | static_cast<int>(b));
}

inline NumericType operator &(NumericType a, NumericType b)
{
    return static_cast<NumericType>(static_cast<int>(a) & static_cast<int>(b));
}

我这样做的原因是:在强类型的C#中,枚举只是一个具有其基础类型字段和一堆在其中定义的常量的结构体。但它可以具有适合于枚举的隐藏字段的任何整数值。

而且似乎C++的枚举也以完全相同的方式工作。在两种语言中,从枚举到int或反之需要进行强制转换。然而,在C#中,默认情况下重载了按位运算符,而在C++中没有。


7
我不确定这是否有意义。个别的"bits"已经被列举出来了,但PadWithZero | NegativeSign = 0x03不是一个有效的枚举常量。 - Useless
1
@Useless 是的,这只是一个例子,基于 Linux 0.1 中用于实现“printf”的 NUMERICTYPE_ 宏定义序列的 C++11 版本。结果必须是原始枚举类型的成员吗?我来自 C# 背景,期望作用域枚举的行为类似于 C# 中的枚举。 - Daniel A.A. Pelsmaeker
4
你的枚举类型在使用 | 运算时不是封闭的,因此将其强制转换为该类型的(非法值)是没有意义的。枚举标志常量值,但将多个标志组合成一个整数。 - Useless
8
@Useless - 这个值 不是 非法的。任何适合该比特位的值都可以。这种比特掩码经常使用,没有什么问题。 - Pete Becker
1
@Useless,0x03为什么是非法的?虽然它不属于列出的类型之一,但我不知道是否有限制enum class中存储0x03的未定义行为... - Yakk - Adam Nevraumont
显示剩余2条评论
6个回答

48
inline NumericType& operator |=(NumericType& a, NumericType b)
{
    return a= a |b;
}

这段代码可用吗?编译并运行:(Ideone)

#include <iostream>
using namespace std;

enum class NumericType
{
    None                    = 0,

    PadWithZero             = 0x01,
    NegativeSign            = 0x02,
    PositiveSign            = 0x04,
    SpacePrefix             = 0x08
};

inline NumericType operator |(NumericType a, NumericType b)
{
    return static_cast<NumericType>(static_cast<int>(a) | static_cast<int>(b));
}

inline NumericType operator &(NumericType a, NumericType b)
{
    return static_cast<NumericType>(static_cast<int>(a) & static_cast<int>(b));
}

inline NumericType& operator |=(NumericType& a, NumericType b)
{
    return a= a |b;
}

int main() {
    // your code goes here
    NumericType a=NumericType::PadWithZero;
    a|=NumericType::NegativeSign;
    cout << static_cast<int>(a) ;
    return 0;
}

打印3。


15
如果枚举类型被定义在一个类内部,并且你想要在这个类中声明运算符,那么就需要将它们声明为“友元”,否则编译器会抱怨运算符有太多的参数——然后它认为这些运算符适用于类而不是枚举类型。或者将运算符声明在类外部,但此时你必须使用类名限定枚举类型名称。如果枚举类型E被定义在类C中,那么该枚举类型的operator|应该在类外部声明为“inline C::E operator|(C::E a, C::E b)”,或在类内部声明为“friend E operator|(E a, E b)”。 - Louis Strous

3

这个对我有效:

NumericType operator |= (NumericType &a, NumericType b) {
    unsigned ai = static_cast<unsigned>(a);
    unsigned bi = static_cast<unsigned>(b);
    ai |= bi;
    return a = static_cast<NumericType>(ai);
}

但是,您仍然可以考虑为enum位的集合定义一个类:

class NumericTypeFlags {
    unsigned flags_;
public:
    NumericTypeFlags () : flags_(0) {}
    NumericTypeFlags (NumericType t) : flags_(static_cast<unsigned>(t)) {}
    //...define your "bitwise" test/set operations
};

然后,将您的|&运算符更改为返回NumericTypeFlags


你的类有一个隐藏的内部字段,就像枚举一样。但是它没有常量,所以一旦你遇到一个期望 NumericTypeFlags 的方法,你的开发环境将无法提供任何帮助。 - Daniel A.A. Pelsmaeker
1
@Virtlink:相比于普通的unsigned,它提供了更好的类型安全性。相比于创建一个数值不在枚举类型中的NumericType,它具有更好的语义。接口的实现可以确保只使用与NumericType兼容的参数进行测试和设置。简而言之,这帮助是为类的用户提供的,代价是实现者需要付出一些工作来使其对用户有所帮助。 - jxh
1
你应该 (a) 返回一个修改后对象的引用并且 (b) 总是转换为 enumstd::underlying_type,而不是假设某个固定类型适用于可以通过按位或组成的所有值。 - underscore_d
@underscore_d: 我同意您的观点。我提供的答案并不是面向C++11的,将值正确地转换为适当的无符号底层类型以进行位运算会复杂化答案的本质。使用容器更好地捕获标志,建议OP考虑使用bitset - jxh

2

我厌倦了所有带有枚举算术的样板文件,并转向更像这样的习惯用法:

struct NumericType {
    typedef uint32_t type;
    enum : type {
        None                    = 0,

        PadWithZero             = 0x01,
        NegativeSign            = 0x02,
        PositiveSign            = 0x04,
        SpacePrefix             = 0x08
    };
};

这样我仍然可以传递NumericType::type参数以提高清晰度,但我会牺牲类型安全性。

我考虑使用通用模板类来替换uint32_t,该类将提供算术重载的一个副本,但显然我不能从类派生枚举(感谢C++!)。


2
阅读完这个问题后,我已经重载了适用于所有枚举类型的按位运算符。
template<typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
inline T operator~(const T& value)
{
    return static_cast<T>(~static_cast<int>(value));
}

template<typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
inline T operator|(const T& left, const T& right)
{
    return static_cast<T>(static_cast<int>(left) | static_cast<int>(right));
}

template<typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
inline T& operator|=(T& left, const T& right)
{
    return left = left | right;
}

template<typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
inline T operator&(const T& left, const T& right)
{
    return static_cast<T>(static_cast<int>(left) & static_cast<int>(right));
}

template<typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
inline T& operator&=(T& left, const T& right)
{
    return left = left & right;
}

template<typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
inline T operator^(const T& left, const T& right)
{
    return static_cast<T>(static_cast<int>(left) ^ static_cast<int>(right));
}

template<typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
inline T& operator^=(T& left, const T& right)
{
    return left = left ^ right;
}

为什么不使用 std::underlying_type<T>::type 而不是 int?通用化一切! - LRDPRDX
1
C++23 中,我们也可以使用 std::to_underlying 来代替(大多数)static_cast<> 调用。 - ATV
1
注意,std::is_enum 对于非作用域枚举和作用域枚举都返回 true。正确的做法是使用 std::is_scoped_enum。 - NN_

0
为什么要强类型?因为我的书说这是良好的实践。
那么你的书并没有涉及到你的使用情况。未经作用域限定的枚举对于标志类型来说是可以的。
enum NumericType : int
{
    None                    = 0,

    PadWithZero             = 0x01,
    NegativeSign            = 0x02,
    PositiveSign            = 0x04,
    SpacePrefix             = 0x08
};

0

通过将不同的值组合成新的未定义值,您完全违反了强类型编程范例。

看起来您正在设置完全独立的单个标志位。在这种情况下,将您的位组合成数据类型并产生未定义值是没有意义的。

您应该决定您的标志数据的大小(charshortlonglong long),并使用它。但是,您可以使用特定的类型来测试、设置和清除标志:

typedef enum
{
    PadWithZero             = 0x01,
    NegativeSign            = 0x02,
    PositiveSign            = 0x04,
    SpacePrefix             = 0x08
} Flag;

typedef short Flags;

void SetFlag( Flags & flags, Flag f )
{
    flags |= static_cast<Flags>(f);
}

void ClearFlag( Flags & flags, Flag f )
{
    flags &= ~static_cast<Flags>(f);
}

bool TestFlag( const Flags flags, Flag f )
{
    return (flags & static_cast<Flags>)(f)) == static_cast<Flags>(f);
}

这是非常基础的,当每个标志只是一个单独的位时就可以了。对于掩码标志,它会更加复杂一些。有一些方法可以将位标志封装到强类型类中,但必须确实值得这样做。在您的情况下,我并不确定它是否值得。


1
我这样做的原因是:在强类型的C#中,枚举只是一个带有其基础类型字段和一堆常量定义的结构体。但它可以具有适合于枚举的隐藏字段的任何整数值。而且似乎C++的枚举也以完全相同的方式工作。在两种语言中,需要进行强制转换才能从枚举到int或反之。然而,在C#中,按位运算符默认情况下被重载,在C++中则不是。顺便说一句... typedef enum { } Flag 不是C++11枚举的语法:enum class Flag { } - Daniel A.A. Pelsmaeker
5
@paddy - 这种情况非常普遍。枚举类型可以表示的值不仅限于命名的枚举器,还可以是适合该枚举类型位数的任何值(口语化表述)。在这里为所有可能的组合提供名称将至少是繁琐的。 - Pete Becker
4
如果您将枚举类型看作一个集合,那么您将得到一些不属于枚举类型属性的约束条件。当然,您可以限制枚举类型的使用以符合这种受限模型。但这并不意味着使用其全部功能的人滥用了它们。标准是经过仔细编写的,允许恰好这种类型的使用。 - Pete Becker
2
这可能一直是一个争论的话题。我的理解是,枚举的语义是提供不同的值,这些值是该类型的唯一可能的值。我仍然认为,虽然将单独的位声明为枚举是完全合法的,但在使用相同的类型存储这些位的组合时,语义上是不正确的。当您破坏语义时,就会违反强类型。所以您必须在两者之间做出选择。您不能将破坏的语义与强类型结合起来使用。 - paddy
9
虽然你有权发表自己的观点,但C++委员会明确并有意地扩展了枚举的范围以涵盖所有二进制组合,但没有包括自动的operator|。这里的逻辑是,只有在有意义时才应该提供operator|,而如果有20个标志组合需要一百万个名称,则不需要拼写出所有名称。 - MSalters
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接