哪些枚举值是C++14中未定义的行为,以及原因

Which enum values are undefined behavior in C++14, and why?

本文关键字:未定义 枚举 C++14      更新时间:2023-10-16

标准中的脚注暗示任何枚举表达式值都是定义的行为;为什么Clang的未定义行为清理程序会标记超出范围的值

考虑以下程序:

enum A {B = 3, C = 7};
int main() {
A d = static_cast<A>(8);
return d + B;
}

未定义行为清理程序下的输出为:

$ clang++-5.0 -fsanitize=undefined -ggdb3 enum.cc && ./a.out 
enum.cc:5:10: runtime error: load of value 8, which is not a valid value for type 'A'

请注意,错误不在static_cast上,而是在加法上。当创建了一个A(但未初始化),然后将值为8的intmemcpy写入A时,情况也是如此——subsan错误在加法上,而不是在初始加载上。

IIUC,较新clangs中的ubsan确实在C++17模式中标记static_cast上的错误。我不知道该模式是否也在memcpy中发现了错误。无论如何,这个问题都集中在C++14上。

报告的错误符合标准的以下部分:

dcl.enum:

对于基础类型固定的枚举,枚举的值是基础类型的值。否则,枚举的值是由具有最小范围指数M的假设整数类型表示的值,使得所有枚举器都可以被表示。足以容纳枚举类型的所有值的最小位字段的宽度为M。可以定义其任何枚举器都未定义值的枚举。如果枚举器列表为空,则枚举的值就好像该枚举有一个值为0的枚举器。100

因此枚举A的值为0到7(包括0到7);范围指数";CCD_ 9为3。根据expr.pre:,评估值为8的A类型的表达式是未定义的行为

如果在表达式求值期间,结果没有在数学上定义,或者不在其类型的可表示值范围内,则行为是未定义的。

但有一个问题:dcl.enum的脚注写道:

这组值用于定义枚举类型的提升和转换语义它并不排除枚举类型的表达式具有超出此范围的值[强调矿]

问题:如果";[dcl.enum]并不排除枚举类型的表达式具有落在此范围"?"之外的值

Clang标记对超出范围的值使用static_cast。如果整数值不在枚举范围内,则行为未定义。

C++标准5.2.9静态转换[expr.Static.cast]第7段

整型或枚举类型的值可以显式转换为枚举类型。如果原始值为在枚举值(7.2)的范围内。否则生成的枚举值未指定/未定义(因为C++17)。

注意脚注100的措辞:"[这组值]并不排除[stuff]。">这不是对"stuff"有效的认可;它只是强调这一部分并没有声明内容无效。事实上,这是一个中立的声明,应该让人想起被排斥的中间派的谬论。就本节而言,枚举值之外的值既不被批准也不被反对。本节定义了哪些值在枚举值之外,但由其他部分(如expr.pre)来决定使用这些值的有效性。

你可以把这个脚注看作是对那些写作编译器的警告:不要想当然!枚举类型的表达式不需要在枚举的值集中有值。除非另一节将这种情况归类为未定义的行为,否则这种情况必须正确编译。


为了更好地理解clang到底在抱怨什么,请尝试以下代码:

enum A {B = 3, C = 7};
int main() {
// Set a variable of type A to a value outside A's set of values.
A d = static_cast<A>(8);
// Try to evaluate an expression of type A with this too-big value.
if ( !static_cast<bool>(static_cast<A>(8)) )
return 2;
// Try again, but this time load the value from d.
if ( !static_cast<bool>(d) ) // Sanitizer flags only this
return 1;
return 0;
}

消毒程序不会抱怨将值8强制输入A类型的变量。它并不抱怨对恰好具有值8(第一个if)的类型A的表达式进行求值。然而,当8的值来自A类型的变量(加载时)时,它确实会抱怨。

我对Clang的编译器并不熟悉,因为我已经习惯了Visual Studio。我目前正在使用Visual Studio 2017。我能够在x86和x64调试版本中编译和运行语言标志设置为c++14和c++17的代码。而不是在您的示例中返回添加:

return d + B;

我决定将它们输出到控制台:

std::cout << (d + B);

并且在所有4种情况下,我的编译器打印出CCD_ 16的值。

我也不确定GCC,因为我还没有在你的例子中尝试过,但这让我相信这是一个依赖于编译器的问题

我关注了你的链接,阅读了你提到的第8节,但从该草案中引起我注意的是来自其他章节的细节,即第7节和第10节。


第7节状态:

对于基础类型不是固定的枚举,基础类型是一个整数类型,可以表示枚举中定义的所有枚举器值。如果没有积分类型可以表示所有枚举器值,则该枚举的格式不正确。它是实现定义的,使用哪种整数类型作为基础类型,除非枚举器的值不能容纳在int或无符号int中,否则基础类型不应大于int。如果枚举器列表为空,则基础类型就像枚举有一个值为0的枚举器一样。

但正是这句话或子句引起了我的注意:

它是实现定义的,哪种整数类型被用作基础类型,除非基础类型不应大于int,除非枚举器的值不能容纳在int或无符号int中。


第10节状态:

枚举器或非范围枚举类型的对象的值通过整数提升转换为整数。[示例:

enum color { red, yellow, green=20, blue };
color col = red;
color* cp = &col;
if (*cp == blue)     // ...

使color成为描述各种颜色的类型,然后将col声明为该类型的对象,将cp声明为指向该类型对象的指针。类型颜色的对象的可能值为红色、黄色、绿色、蓝色;这些值可以转换为积分值0、1、20和21。由于枚举是不同的类型,因此只能为颜色类型的对象分配颜色类型的值。

color c = 1;        // error: type mismatch, no conversion from int to color
int i = yellow;     // OK: yellow converted to integral value 1, integral promotion

请注意,这种隐式枚举到int的转换不是为作用域枚举提供的:

enum class Col { red, yellow, green };
int x = Col::red;   // error: no Col to int conversion
Col y = Col::red;
if (y) { }          // error: no Col to bool conversion

--结束示例]

正是这两行引起了我的注意:

color c = 1;        // error: type mismatch, no conversion from int to color
int i = yellow;     // OK: yellow converted to integral value 1, integral promotion

所以让我们回顾一下您的例子:

enum A {B = 3, C = 7};
int main() {
A d = static_cast<A>(8);
return d + B;
}

这里A是一个完整类型,B&CCD_ 19是通过提升被评估为积分类型的常数表达式并且相应地被设置为37的值的枚举器。这包括enum A{...};的申报

main()内部,您现在声明了A的一个实例或对象,称为d,因为A是一个完整的类型。然后通过static_cast的机制为d指定一个值8,它是一个常量表达式或常量文字。我不能100%确定每个编译器是否以完全相同的方式执行static_cast;我不确定这是否与编译器有关。

因此,dA类型的对象,但由于值8不在枚举列表中,我认为这属于implementation defined子句的范围。这应该会将d提升为积分类型。

然后在您的最后陈述中返回d+B

假设d被提升为值为8的整数类型,那么您将B的枚举值3添加到8,因此您应该得到11的输出,我在visual studio上的所有4个测试用例中都有CCD_42。

现在,对于您的Clang编译器,我不能说,但据我所知,至少根据Visual Studio,这似乎不会产生任何错误或未定义的行为。再说一遍,因为这段代码似乎是由实现定义的,我认为这在很大程度上取决于您的特定编译器及其版本,以及您编译它所使用的语言版本。

我不能说这会完全回答你的问题,但也许它会根据草案的文件对编译器的基本工作原理有所了解;标准。


-编辑-

我决定通过我的调试器运行它,并在这条线上设置了一个断点:

A d = static_cast<A>(8);

然后我执行了这行代码,并查看了调试器中的值。在VisualStudio中,d的值确实为8。然而,在其类型下,它被列为A,而不是int。所以我不知道这是否是将其提升到int,或者它是否碰巧是编译器优化,比如asm,它将d视为intunsigned int,等等。;但Visual Studio允许我通过CCD_ 52向枚举类型分配一个整数值。但是,如果我删除static_cast,它确实无法编译,说明您不能将类型int分配给类型A

这让我相信,我上面最初的说法实际上是不正确的,或者只是部分正确。编译器并没有在赋值时将其完全"提升"为整数类型,因为d仍然是A的实例,除非它是在我不知道的情况下这样做的。

我还没有查看此代码的asm,以了解Visual Studio正在生成哪些汇编指令。。。因此,我目前还不能作出全面评估。现在,稍后如果我有更多的可用时间;我可以查看它,看看我的编译器正在生成哪些asm行,看看编译器正在采取的基本操作。