【问题标题】:Is it legal to cast to enum values not representable by enum?强制转换为无法由枚举表示的枚举值是否合法?
【发布时间】:2016-09-12 17:03:55
【问题描述】:

给定enum class val { foo = 1, bar = 2, baz = 4 };

可以定义:

val operator|(val x, val y)
{
    return static_cast<val>(static_cast<int>(x) | static_cast<int>(y));
}

但是,这样做在语义上是否正确?

我倾向于,如下面的看似良好的示例所示:

int convert(val x)
{
    switch(x)
    {
    case val::foo: return 42;
    case val::bar: return 53;
    case val::baz: return 64;
    }
}

当使用 g++ 编译并且使用 clang++ 进行分段错误时,调用 convert(val::foo | val::bar) 将返回 0

Here 是 g++ 版本。而here是clang++版本。

我的问题有两个:

  1. 在枚举中存储不由枚举器表示的值在语义上是否正确?我们非常欢迎标准摘录。

1.a 上面链接的例子中哪个编译器是正确的,g++还是clang++?

  1. 是否有标准(或建议的)方法来表示 C++ 中的标志?

我能想到几种可能的实现方式:

enum class val { foo, bar, baz, size };
using val_flags = std::set<val>; // (1)
using val_flags = std::vector<bool>; // (2)
using val_flags = std::bitset<val::size>; // (3)
using val_flags = std::underlying_type<val>::type; // (4)

更新:

谢谢大家的回答。我最终恢复了我的旧枚举运算符模板。如果有人感兴趣,可以在这里找到:github.com

【问题讨论】:

  • 我认为您在那里可能有未定义的行为(假设 convert 是合法的):x 不适合 val 中的任何值,因此由于缺少返回,您会得到未定义的行为声明。
  • 我在这里看到的唯一问题是int convert(val x) 有可能不返回任何值。我很确定您的编译器对此发出了警告。

标签: c++ c++11 enums


【解决方案1】:

以下看似行为良好的示例:

不是,而是做一个小改动:

int convert(val x)
{
    switch(x)
    {
    case val::foo: return 42;
    case val::bar: return 53;
    case val::baz: return 64;
    }

    return 9; // ADDED THIS LINE
}

一切都会好起来的。另一种解决方法是使用default: 案例并返回那里。

您现有的代码通过到达具有非void 返回类型的函数的右大括号来触发未定义的行为1。因为它是未定义的行为,所以两个编译器都是正确的。

enum 类型中保存值的语义是枚举数值的按位或组合,是明确定义和保证的。该标准要求enum 的实例可以存储任何整数值,使用的位数不超过定义的任何枚举值,其中包括所有按位或组合。用正式语言说这有点混乱,但在这里(注意你的案例是enum class,这些总是有固定的底层类型,第一句话适用):

对于基础类型固定的枚举,枚举的值是 基础类型。否则,对于其中 emin 是最小枚举数且 emax 是 最大,枚举的值是 bmin 到 bmax 范围内的值,定义如下:设 K 对于二进制补码表示为 1,对于一个补码或符号幅度表示为 0。 bmax 是大于等于 max(|emin| - K, |emin|) 且等于 2 的最小值M - 1,其中 M 是一个非负整数。如果 emin 为非负数,则 bmin 为零,否则为 -(bmax + K)。的大小 如果 bmin 是,则足以容纳枚举类型的所有值的最小位域是 max(M, 1) 零和 M + 1 否则。可以定义一个枚举,它的任何枚举器都没有定义它的值。如果 enumerator-list 为空,则枚举的值就好像该枚举有一个值为 0 的枚举数。

(来自 n4582,第 7.2 节 [dcl.enum]


1 从 6.6.3 [stmt.return]:

从构造函数、析构函数或具有 cv void 返回类型的函数的末尾流出等效于没有操作数的返回。否则,从 main (3.6.1) 以外的函数末尾流出会导致未定义的行为。

【讨论】:

  • 或者您可以明确说明底层类型并说enum class val : std::uint16_t { ... }; 或其他任何内容。如果我打算对原始值进行按位运算,我更喜欢使用 unsigned 类型。
  • @Ben,感谢您的回答。那么,从中得出的结论是,测试一个枚举的所有枚举数是不够的?是否应该始终添加default: 子句(或类似子句)以防有人传递了枚举器未涵盖的任意值?
  • @InnocentBystander 在这种特殊情况下,默认情况就足够了。一个经验法则是总是返回一些东西,不管你相信什么假设是正确的。另一个经验法则是阅读编译器警告,因为这会告诉你是否忘记返回某些内容。
  • @Nelxost, "... 阅读您的编译器警告..." 这很有趣,我使用 clang 已经有一段时间了,但它并没有给出在这种特殊情况下发出警告。我什至在使用 -Weverything(除了少数例外)。我只注意到使用 gcc 在 colieru 上尝试我的示例的警告。 Clang 缺乏警告实际上引发了我的问题。
  • @InnocentBystander 确实,看起来 Clang 更宽容。它看到每个枚举器案例都被处理并停在那里,而 GCC 尝试每个可能的值,即使是枚举。如果你想继续使用 Clang,我想你必须记住第一条经验法则。
【解决方案2】:

在枚举中存储不由枚举器表示的值在语义上是否正确?标准的摘录是最受欢迎的。

如果值在枚举范围内,则为是。 Ben Voigt 提供了标准的摘录。我更喜欢看cppreference,因为我发现它更具可读性(尽管它没有相同的权威价值)。

整数、浮点数和其他枚举类型的值可以转换为任何枚举类型,例如通过 static_cast。如果转换为枚举的基础类型的值超出此枚举的范围,则结果为未指定 (C++17 前)未定义行为 (C++17 起)。如果基础类型是固定的,则范围是基础类型的范围。如果基础类型不固定,则范围是最小位字段的所有可能值,该最小位字段大到足以容纳目标枚举的所有枚举数。请注意,此类转换后的值不一定等于为该类型定义的任何命名枚举数。

 

以上链接示例中哪个编译器是正确的,g++ 还是 clang++?

问题在于您的代码调用了未定义的行为,但原因与枚举无关,正如 cmets 和 Ben Voigt 所指出的那样。所以两个编译器都是正确的。

请注意,您实际上并不需要 convert 函数来试验这些行为。

enum class val { foo = 1, bar = 2, baz = 4 };

val operator|(val x, val y) {
    return static_cast<val>(static_cast<int>(x) | static_cast<int>(y));
}

int main() {
    std::cout << static_cast<int>(val::foo | val::bar); // prints 3
}

Live example

是否有标准(或建议的)方法来表示 C++ 中的标志?

我会在范围的结构(或类)中使用静态 constexpr 变量。

struct Flags {
    static constexpr unsigned int foo = 0x01;
    static constexpr unsigned int bar = 0x02;
    static constexpr unsigned int baz = 0x04;
};

【讨论】:

  • 但请注意,符号常量方法不提供类型安全或允许重载。
  • @BenVoigt 好点,虽然我在处理标志时从来不需要这些。话虽如此,我并不经常处理标志,主要是因为通常有更好的解决方案。
【解决方案3】:

除了其他答案(已经指出问题),如果您需要一组标志,请考虑使用std::bitset,并最终使用enum 为位命名(而不是它们的重量),喜欢

enum flags_names { f0, f1, f2, f3, NFlags }
typedef std::bitset<NFlags> flags;

现在你可以做

flags fs;
fs[f0] = true; fs[f2] = false;
if(fs[f0]) ...
etc.

【讨论】:

  • 谢谢埃米利奥。我使用bitsetvector&lt;bool&gt;set&lt;val&gt; 时遇到的唯一问题是它们不如enums 轻量级。虽然枚举可以在寄存器中传递,但所有其他解决方案都需要内存分配、通过引用传递等。
  • 我正准备使用bitset,因为我注意到它的许多功能都是constexpr。然而,对我来说真正的交易破坏者是无法从initializer_list 构建它。类似:flags = { f0, f2 }。无论如何感谢您的回答和我的 +1
猜你喜欢
  • 1970-01-01
  • 2016-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多