【问题标题】:Which enum values are undefined behavior in C++14, and why?哪些枚举值在 C++14 中是未定义的行为,为什么?
【发布时间】:2019-01-26 15:50:12
【问题描述】:

标准中的脚注暗示任何枚举表达式值都是定义的行为;为什么 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 的 intmemcpyied 放入 A 时也是如此 - ubsan 错误在于添加,而不是初始加载。

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

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

dcl.enum:

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

因此枚举 A 的值是 0 到 7(含),而“范围指数”M 是 3。根据 expr.pre 评估具有值 8 的 A 类型的表达式是未定义的行为:

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

但是有一个小问题:footnote from dcl.enum 写着:

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

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

【问题讨论】:

  • 它标记了 static_cast。
  • @jbapple:注释不规范。因此,您不能依赖它们来进行明确定义的行为。
  • @supercat:感谢您的评论。举这个具体的例子。 Clang 给出了运行时错误,但 GCC 没有。我查阅了 GCC 手册,据我所知,他们没有记录任何不这样做的理由。那么,我们可以得出结论说 GCC 不是一个高质量的实现吗? GCC 会在此基础上接受错误报告吗?我认为不会。
  • @PW:在标准没有强加任何要求但大多数针对特定目的的实现以一致的方式表现的地方,任何旨在适合该目的但表现不同的质量实现应该记录这样做的充分理由。据我所知,大多数实现将为每个枚举选择一个足够大以容纳其所有值的整数类型,使用该类型存储枚举,并允许将适合该类型的任何值存储在枚举中。在某些情况下,其他行为可能更有用,并且...
  • ...如果实现以某种其他方式表现出能够更好地满足其客户需求的行为,那么它不会有任何“错误”,前提是它记录了该行为,并且假设替代行为确实如此满足客户的需求。

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


【解决方案1】:

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

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

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

【讨论】:

  • 注意错误不在static_cast上,而是在加法上。这也是正确的,即创建了一个A(但未初始化),然后一个值为 8 的intmemcpyied 进入A - ubsan 错误在于添加,而不是初始加载。 IIUC,ubsan 确实在 C++17 模式下的 static_cast 上标记了一个错误。不知道那个模式是不是也发现memcpy有错误。
  • value is within the range of the enumeration values 是什么意思? 3 对enum{A,B,C} 有效吗?该类型应该包含 2^2 个值,所以我猜是的。 enum class: uint16_t{A,B,C} 的 256 怎么样?它当然适合底层类型,但超出了枚举数。
  • @Flamefire 3enum {A, B ,C} 无效。 256 对enum : uint16_t{A, B, C} 有效。 “枚举值”实际上是您在枚举中定义的值。 “枚举值范围内的值”是等于枚举值之一的值。见前。 cppreference enum。我认为 cppreference 掌握得最好:If the underlying type is not fixed and the source value is out of range, the result is unspecified (until C++17)undefined (since C++17).
  • 如果我正确阅读了该参考链接,那么3 是有效的,因为范围是0..3,其中3 在里面。另请参阅句子“注意,这种转换后的值可能不一定等于为枚举定义的任何命名枚举数。”其中7 用作enum { A = 1, B = 2, C = 4 } 的示例
  • 你是对的,我错了。 enum{A, B, C} 的范围必须为 2 位,因此 3 将是值。关键似乎是 - 再次引用 cppreference - if it would fit in the smallest bit field large enough to hold all enumerators
【解决方案2】:

注意脚注 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 的 A 类型表达式求值(第一个 if)。但是,当 8 的值来自(加载来自)A 类型的变量时,它确实会抱怨。

【讨论】:

  • “脚注 100 如何确认这是超出可表示值范围的问题。”?对我来说,那个脚注似乎只暗示什么不是问题。
  • @jbapple “这组值用于定义枚举类型的提升和转换语义。”在提升语义期间,我们正在处理这组值之外的值。因此出现了问题。
  • @jbapple 另外,脚注 100 的第二句并不意味着什么不是问题。我已经修改了我的答案以涵盖这一点。
  • C 和 C++ 标准的作者并没有努力完全指定质量实现应该以相同方式表现的所有情况除非他们有并记录了一个令人信服的理由否则 [不一定保证对结果行为有用]。编译器编写者将语言限制为标准明确定义的结构已成为一种时尚,但至少对于 C 标准,这种解释直接违背了作者在已发布的基本原理文档中的意图。
  • @supercat 我无法解析第一句话,这可能是我提出问题的原因:这与我的答案有什么关系?
【解决方案3】:

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

return d + B;

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

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

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

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

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


第 7 节 声明:

对于底层类型不固定的枚举,底层类型是一个整数类型,可以表示枚举中定义的所有枚举数值。如果没有整数类型可以表示所有枚举数值,则枚举格式错误。使用哪种整数类型作为基础类型由实现定义,除非基础类型不得大于 int,除非枚举数的值不能适合 int 或 unsigned int。如果 enumerator-list 为空,则基础类型就好像枚举有一个值为 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是枚举器,它们通过提升被评估为整数类型的常量表达式,并相应地设置为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 个测试用例中都有。

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

我不能说这会完全回答你的问题,但也许它会根据草案和标准的文档对编译器的下划线工作有所了解。


-编辑-

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

A d = static_cast<A>(8);

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

这让我相信我上面的原始陈述实际上是不正确的,或者只是部分正确。编译器在赋值时并未将其完全“提升”为整数类型,因为d 仍然是A 的实例,除非它在我不知道的幕后这样做。

我还没有查看此代码的asm 以了解 Visual Studio 正在生成哪些汇编指令...因此我目前无法进行全面评估。现在,以后如果我有更多的空闲时间;我可能会查看它以查看我的编译器正在生成哪些 asm 行,以了解编译器正在执行的基本操作。

【讨论】:

  • 是的,这是一个依赖于编译器的问题。这就是为什么这是一个关于 Clang 的 未定义行为清理程序的问题。如果您使用 clang 而不是 sanitizer(省略 -fsanitize=undefined 选项),示例程序运行时不会出现错误。如果你使用 clang 和 sanitizer,你会得到错误并且返回值 11。(错误不是致命的。)
  • @JaMiT 哦,好吧,现在说得通了,我是从这个问题中了解到的......
猜你喜欢
  • 2018-10-15
  • 1970-01-01
  • 1970-01-01
  • 2013-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-09
相关资源
最近更新 更多