在 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:我忽略了 e1
或 e2
的情况,并且没有默认情况。
<source>:4:12: warning: enumeration value 'e2' not handled in switch [-Wswitch]
switch(e){
这种行为很棒,因为
- 它在编译时检查枚举和开关之间的一致性,使它们成为非常有用且不可分割的一对功能。
- 我不需要定义一个人为的
default
案例,对此我没有好事可做。
- 它允许我省略全局 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
}
这与 enum
或 switch
无关,而与编译器通过每条路径证明有效 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();
}
请务必注意,根据您对 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 成为现实。
所以目前,您只能使用特定于编译器的替代方案。
我通常使用 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:我忽略了 e1
或 e2
的情况,并且没有默认情况。
<source>:4:12: warning: enumeration value 'e2' not handled in switch [-Wswitch]
switch(e){
这种行为很棒,因为
- 它在编译时检查枚举和开关之间的一致性,使它们成为非常有用且不可分割的一对功能。
- 我不需要定义一个人为的
default
案例,对此我没有好事可做。 - 它允许我省略全局 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
}
这与 enum
或 switch
无关,而与编译器通过每条路径证明有效 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();
}
请务必注意,根据您对 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 成为现实。
所以目前,您只能使用特定于编译器的替代方案。