对clang、gcc和icc中开关枚举类返回的处理一致



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

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
}
}

如果:我省略了e1e2的情况,则clang会抱怨(警告(,并且没有默认情况。

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

这种行为很棒,因为

  1. 它在编译时检查枚举和开关之间的一致性,使它们成为一对非常有用且不可分割的特性
  2. 我不需要定义一个人工default的情况,对此我没有什么好做的
  3. 它允许我省略一个全局返回,对于它我没有什么好的返回(有时返回不是像int这样的简单类型,它可能是一个没有默认构造函数的类型

(注意,我使用的是enum class,所以我假设只有有效的情况,因为无效的情况只能由呼叫者端的恶劣强制转换生成。(

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

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

我为这项工作找到的唯一解决方案是既有default的情况,又返回非意义的值。

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
}
}

这很糟糕,因为我上面提到的原因(甚至没有涉及到返回类型没有默认构造函数的情况(。但这也很糟糕,因为我可以再次忘记其中一个枚举情况,现在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

需要注意的是,给定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。

因此,就目前而言,您只能使用编译器特定的替代方案。

这三个编译器都有__builtin_unreachable()扩展名。您可以使用它来抑制警告(即使返回值存在构造函数问题(,并引发更好的代码生成:
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

这与enumswitch无关,一切都与编译器通过每条路径证明有效返回语句的能力有关。有些编译器比其他编译器更擅长这一点。

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

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

编辑:如果您从无法访问的路径返回,一些编译器(如MSVC(会抱怨。对于编译器,只需将返回值加上#if即可。或者,正如我经常做的那样,只需要定义一个基于编译器定义的RETURN(x(。

最新更新