哪些枚举值是C ++ 14中未定义的行为,为什么?

时间:2019-01-26 15:50:12

标签: c++ enums c++14 language-lawyer undefined-behavior

标准中的脚注表示任何枚举表达式值都是定义的行为;为什么Clang的未定义行为消毒剂会标记超出范围的值?

考虑以下程序:

enum A {B = 3, C = 7};

int main() {
  A d = static_cast<A>(8);
  return d + B;
}

the undefined behavior sanitizer下的输出为:

$ 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的int memcpy放入A中的情况下,也确实存在ubsan错误另外,不是初始负载。

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

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

dcl.enum

  

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

因此,枚举A的值是0到7,包括0和7,“范围指数” M是3。未定义类型为A且值为8的表达式根据{{​​3}}的行为:

  

如果在对表达式进行求值时,未在数学上定义结果,或者该结果不在其类型的可表示值范围内,则行为不确定。

但是有一个小问题:expr.pre读为:

  

这组值用于为枚举类型定义升级和转换语义。 它并不排除枚举类型的表达式的值超出此范围。 [强调我的意思]

问题: 如果“ [dcl.enum]不排除枚举类型的表达式的值不在此范围之内,为什么是值为8且类型为A的表达式未定义的行为范围”?

3 个答案:

答案 0 :(得分:5)

Clang会在超出范围的值上标记使用static_cast。如果积分值不在枚举范围内,则行为是不确定的。

  

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

     

整数或枚举类型的值可以显式转换为   枚举类型。如果原始值为   在枚举值(7.2)的范围内。否则,   结果枚举值是不确定的/未定义的(从C ++ 17开始)。

答案 1 :(得分:3)

请注意脚注100的措词:“ [这组值]不排除[stuff]。” 这并不表示对“ stuff”的认可;它只是强调本节不会声明这些内容无效。实际上,应该想到fallacy of the excluded middle的中性声明。就本节而言,枚举值之外的值都不会被批准也不会被拒绝。本节定义了哪些值不在枚举值的范围内,但由其他节(如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类型的变量(是从加载)时,它确实会抱怨。

答案 2 :(得分:0)

由于我习惯于Visual Studio,因此我对Clang的编译器并不十分熟悉。我当前正在使用Visual Studio2017。在x86和x64调试版本中,我都能使用设置为c ++ 14和c ++ 17的语言标志编译并运行您的代码。而不是在您的示例中返回添加内容:

return d + B;

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

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

在所有4种情况下,我的编译器都打印出11的值。

我不确定GCC,因为我还没有尝试使用您的示例,但这使我相信这是编译器相关的问题。

我已经按照您的链接阅读了您提到的第8节,但是引起我注意的是该草案的其他第7节和第10节的细节。


第7节 状态:

  

对于基础类型不固定的枚举,基础类型是整数类型,可以表示该枚举中定义的所有枚举器值。如果没有整数类型可以表示所有枚举数,则枚举格式不正确。由实现定义的是哪种整数类型用作基础类型,除非基础类型不得大于int,除非枚举器的值不能适合int或unsigned int。如果枚举数列表为空,则基础类型就像枚举有一个值为0的枚举数一样。

但这是引起我注意的句子或从句:

  

由实现方式定义哪种整数类型用作基础类型,除非基础类型不得大于int,除非枚举数的值不能适合int或unsigned 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类型的对象分配color类型的值。

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是完整类型,BC是枚举,通过提升将其评估为整数类型的常量表达式,并将其设置为{{1 }}和3。这涵盖了7

的声明

由于enum A{...};是完整类型,因此现在在main()内声明名为A的{​​{1}}的实例或对象。然后,通过d的机制为A分配一个值d,该值是一个常量表达式或常量文字。我不确定100%是否每个编译器都以完全相同的方式执行8;我不确定这是否取决于编译器。

因此static_cast是类型static_cast的对象,但是由于值d不在枚举列表中,因此我认为这属于A的子句。然后应将8提升为整数类型。

然后在您的最终声明中返回implementation defined

假设将d提升为值为d+B的整数类型,然后将d的枚举值8添加到{{1 }},因此您应该获得B的输出,其中在Visual Studio的所有4个测试用例中都有我的输出。

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

我不能说这将完全回答您的问题,但是也许可以根据草案和标准的文档对编译器的基本工作有一些了解。


-编辑-

我决定通过调试器运行此程序,并在此行上设置一个断点:

3

然后,我逐步执行了这一行代码,并查看了调试器中的值。在Visual Studio中,8的值为11。但是,在其类型下,它被列为A d = static_cast<A>(8); ,而不是d。因此,我不知道这是否将其提升为8,或者它是否可能是编译器优化,而像A这样的东西正在处理int作为intasm等。但是Visual Studio允许我通过d将整数值分配给枚举类型。但是,如果我删除了int,它并不能编译,说明您不能将类型unsigned int分配给类型static_cast

这使我相信我上面的原始说法实际上是不正确的或仅部分正确。编译器在分配时并未完全将其“提升”为整数类型,因为static_cast仍然是int的实例,除非在我不知道的情况下这样做。

我还没有查看该代码的A来查看Visual Studio正在生成哪些汇编指令...因此,目前我目前无法进行完整的评估。现在,以后如果我有更多时间可用的话;我可能会查看它,以查看我的编译器正在生成哪些d行,以查看编译器正在执行的基本操作。