使用枚举类来标记未定义的行为吗?

Is using enum class for flags undefined behavior?

提问人:GLJeff 提问时间:1/2/2023 最后编辑:ElliottGLJeff 更新时间:1/3/2023 访问量:215

问:

我一直在使用重载运算符,如这里的第二个答案所示:如何使用 C++11 枚举类作为标志......例:

#define ENUMFLAGOPS(EnumName)\
[[nodiscard]] __forceinline EnumName operator|(EnumName lhs, EnumName rhs)\
{\
    return static_cast<EnumName>(\
        static_cast<std::underlying_type<EnumName>::type>(lhs) |\
        static_cast<std::underlying_type<EnumName>::type>(rhs)\
        );\
}...(other operator overloads)

enum class MyFlags : UINT //duplicated in JS
{
    None = 0,
    FlagA = 1,
    FlagB = 2,
    FlagC = 4,
};
ENUMFLAGOPS(MyFlags)

...

MyFlags Flags = MyFlags::FlagA | MyFlags::FlagB;

我开始担心这可能会产生未定义的行为。我看到它提到,仅仅有一个不等于定义的枚举值之一的枚举类变量是未定义的行为。在本例中,Flags 的基础 UINT 值为 3。这是未定义的行为吗?如果是这样,在 c++20 中执行此操作的正确方法是什么?

C++ C++20 未定义行为 枚举类

评论


答:

11赞 bolov 1/2/2023 #1

一种误解是枚举类型只有它声明的值。 枚举具有基础类型的所有值。只是在枚举中,其中一些值具有名称。通过 ing 获取没有名称的值是完全可以的,或者在经典枚举的情况下,通过运算 () 或简单赋值来获取。static_cast|

您的代码非常好(除了可能引起一些对宏使用的关注)。

9.7.1 枚举声明 [dcl.enum]

  1. 对于基础类型为固定的枚举,枚举的值是基础类型的值。

对于基础类型不固定的枚举(即 is missing)标准说的基本上是一样的,但以一种更复杂的方式:枚举与基础类型具有相同的值,但关于基础类型是什么有更多的规则。: std::uint32_t


这超出了您的问题范围,但您可以在没有任何宏的情况下定义运算符,我强烈推荐它:

template <class E>
concept EnumFlag = std::is_enum_v<E> && requires() { {E::FlagTag}; };

template <EnumFlag E>
[[nodiscard]] constexpr E operator|(E lhs, E rhs)
{
    return static_cast<E>(std::to_underlying(lhs) | std::to_underlying(rhs));
}

enum class MyFlags : std::uint32_t
{
    None = 0x00,
    FlagA = 0x01,
    FlagB = 0x02,
    FlagC = 0x04,

    FlagTag = 0x00,
};

是的,您可以有多个具有相同值的“名称”(枚举器)。因为我们不使用价值,所以它有什么价值并不重要。FlagTag

要标记要为其定义运算符的枚举,可以使用上面示例中的标记,也可以使用类型特征:

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

template <>
struct is_enum_flag<MyFlags> : std::true_type {};

template <class E>
concept EnumFlag = is_enum_flag<E>::value;

评论

0赞 GLJeff 1/2/2023
std::to_underlying 需要 c++23,所以我仍然必须使用繁琐的 static_cast<std::underlying_type<E>::type>但我喜欢概念的使用并采用了它。
1赞 GLJeff 1/2/2023
还有一个有趣的说明,我问了一个非常受欢迎的编码大师这个问题,他们回答说:“是的,在枚举类中使用与现有枚举器不对应的值是未定义的行为。枚举器是一个命名常量,其值在枚举定义中指定。他们的名字是ChatGpt。摇摇头
1赞 bolov 1/2/2023
@GLJeff我个人并不在我在这里看到的 ChatGPT 仇恨列车上。虽然它有缺陷,但我认为它是一项了不起的技术,一种概念证明。如果您盲目地信任它并且不知道如何检查其输出,那么是的,这很危险。但是,如果您聪明地使用它,它实际上可以成为一个有用的工具。例如,我给它喂了我的,并要求它写下所有的运算符。它做到了。正确。二进制和一元运算符。和复合赋值运算符。我告诉它这是一个枚举的概念,它只知道编写与枚举相关的二进制运算符。我觉得这令人印象深刻。operator|EnumFlag
1赞 bolov 1/2/2023
在复合赋值上,它知道定义了非赋值等效运算符并直接在枚举上使用它们,没有使用 和 .我告诉你,我一直对此感到惊讶。事实上,您可以与它交谈并进行改进。to_underlyingstatic_cast
1赞 bolov 1/2/2023
是的,在很多情况下,它失败了,它对自己的失败充满信心。但我不能看不到它纯粹的惊人部分。
1赞 supercat 1/3/2023 #2

C 和 C++ 标准都没有区分那些在不了解类型位模式和相关陷阱表示(或缺乏)的情况下单独检查将是未定义行为的行为,以及那些“未定义性”胜过人们对此类事物的任何了解的行为。C99标准改变了何时为负数的处理方式,也许最能说明这一理念;我 C++ 标准可能有一些更好的例子,但我对它并不熟悉。x<<1x

如果一个平台有一个比普通指令更快的 8 位存储指令,除了尝试存储 1100 0000 的位模式会导致 CPU 过热和熔化,我认为 C++ 标准中的任何内容都不会禁止 C++ 实现提供使用该存储指令的扩展类型, 并使用这样的类型来表示其类型未指定,其值包括 -63 和 -62,但不包括 -64。如果不能确定代码不会在这样的平台上运行,就无法知道尝试分别执行 when 和 hold -63 和 -62 是否会让 CPU 着火。因此,就标准而言,后一种结构将是未定义的行为。int_least7_tenummyEnum1 = (myEnumType)((int)myEnum2 & (int)myEnum3);myEnum2myEnum3

C 和 C++ 标准都陷入了两难境地,有些人认为标准不应该添加新的文本来说明几十年来一直处理的结构应该继续存在,而另一些人则认为缺乏任何此类授权是邀请将长期实践抛在窗外。要知道是否应该期望任何特定的结构起作用,唯一的方法是知道负责目标实现的人是否尊重先例或将其视为“优化”的障碍。

评论

0赞 GLJeff 1/3/2023
这个答案是为了回应我的评论,要求澄清在未指定基础类型时使用其明确请求范围之外的枚举是否是未定义的行为,对吗?答案是,是的,根据定义,它在技术上必须是。(同样在这种情况下,我向 ChatGPT 提出的问题不够具体,因为我没有具体说明是否明确定义了枚举类的基础类型。
0赞 supercat 1/3/2023
@GLJeff:无论出于何种原因,该标准都试图避免提及其行为特征无法在所有情况下定义的操作,即使在 99.9999% 的实际情况下,所有非人为的实现都会以相同的方式处理它们。