在 clang、gcc 和 icc 中一致地处理开关枚举 class returns

Handling of switch enum class returns in clang, gcc and icc consistently

我通常使用 clang 来开发代码,尽我所能使用所有合理的警告 (-Wall -Wextra [-Wpedantic])。这个设置的好处之一是编译器检查 switch stataments 与所用枚举相关的一致性。例如在这段代码中:

enum class E{e1, e2};

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22; // if I forget this line, clang warns
    }
}

clang 会抱怨(警告)if:我忽略了 e1e2 的情况,并且没有默认情况。

<source>:4:12: warning: enumeration value 'e2' not handled in switch [-Wswitch]
    switch(e){

这种行为很棒,因为

  1. 它在编译时检查枚举和开关之间的一致性,使它们成为非常有用且不可分割的一对功能。
  2. 我不需要定义一个人为的 default 案例,对此我没有好事可做。
  3. 它允许我省略全局 return ,我对 return 没有好处(有时 return 不是像 [=26 这样的简单类型=], 例如,它可以是没有默认构造函数的类型。

(请注意,我使用的是 enum class,因此我只假设有效情况,因为无效情况只能由调用方端的恶意转换生成。)

现在是坏消息: 不幸的是,当切换到其他编译器时,这很快就会崩溃。 在 GCC 和 Intel (icc) 中,上面的代码警告(使用相同的标志)我不是 return 来自非空函数。

<source>: In function 'int fun(E)':
<source>:11:1: warning: control reaches end of non-void function [-Wreturn-type]
   11 | }
      | ^
Compiler returned: 0

我为这项工作找到的唯一解决方案既有 default 案例又有 return 无意义的值。

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
        default: return {}; // or int{} // needed by GCC and icc
    }
}

由于我上面提到的原因,这很糟糕(甚至没有涉及 return 类型没有默认构造函数的情况)。 但这也很糟糕,因为我可以再次忘记其中一个枚举案例,现在 clang 不会抱怨,因为有一个默认案例。

所以我最终做的是让这些丑陋的代码在这些编译器上工作,并在它可以出于正确原因时发出警告。

enum E{e1, e2};

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
#ifndef __clang__    
        default: return {};
#endif
    }
}

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
    }
#ifndef __clang__    
    return {};
#endif
}

有更好的方法吗?

这是示例:https://godbolt.org/z/h5_HAs


非默认构造的情况下 类我真的完全没有好的选择:

A fun(E e){
    switch(e){
        case E::e1: return A{11};
        case E::e2: return A{22};
    }
#ifndef __clang__
    return reinterpret_cast<A const&>(e);  // :P, because return A{} could be invalid
#endif
}

https://godbolt.org/z/3WC5v8

这与 enumswitch 无关,而与编译器通过每条路径证明有效 return 语句的能力有关。有些编译器在这方面比其他编译器做得更好。

正确的方法是在函数的末尾添加一个valid return

A fun(E e){
  switch(c){
    case E::e1: return A{11};
    ...
  }
  return A{11}; // can't get here, so return anything
}

编辑:如果您从无法访问的路径获得 return,一些编译器(如 MSVC)会报错。只需为编译器用 #if 将 return 括起来。或者像我经常做的那样,只定义一个基于编译器定义的 RETURN(x)。

这三个编译器都有 __builtin_unreachable() 扩展名。您可以使用它来抑制警告(即使 return 值存在构造函数问题)并引发更好的代码生成:

enum class E{e1, e2};


int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
    }
    __builtin_unreachable();

}

https://godbolt.org/z/0VP9af

请务必注意,根据您对 fun 的初始定义,完全合法的 C++ 可以执行以下操作:

fun(static_cast<E>(2));

任何枚举类型都可以在其表示的位数范围内采用任何值。具有显式基础类型(enum class 总是 具有基础类型;默认情况下 int )的类型的表示是该基础类型的整体。因此,默认情况下 enum class 可以假定任何 int.

的值

这是不是 C++ 中的未定义行为。

因此,GCC 完全有权假设 fun 可以获得其基础类型范围内的任何值,而不仅仅是其枚举数之一。

标准 C++ 对此没有真正的答案。在一个理想的世界中,C++ 会有一个契约系统,您可以在其中预先声明 fun 要求参数 e 是枚举数之一。有了这些知识,GCC 就会知道开关将采用所有控制路径。当然,即使 C++20 有契约(正在为 C++23 重新设计),仍然没有办法测试枚举值是否仅具有等于其枚举数之一的值。

在一个不太理想的世界中,C++ 将有一种方法可以显式地告诉编译器一段代码预计无法到达,因此编译器可以忽略执行到达那里的可能性。不幸的是,该功能也没有使 C++20 成为现实。

所以目前,您只能使用特定于编译器的替代方案。