我正在使用较旧的gcc版本在LINUX中开发一个应用程序(7.如果我没记错的话,有些东西)。最近我试图在Windows上运行相同的应用程序。在Windows上,我使用MinGW作为编译器(使用gcc 8.1.0)。
我在Windows上编译应用程序时遇到此错误消息:
警告:控制到达非空函数的末尾[-Wreturn-type]
代码类似于以下内容:
class myClass {
protected:
enum class myEnum{
a,
b,
};
int fun(myClass::myEnum e);
}
和
int myClass::fun(myClass::myEnum e) {
switch (e){
case myEnum::a:{
return 0;
}
case myEnum::b:{
return 1;
}
}
}
我理解错误信息意味着什么,我只是想知道为什么它在LINUX中从来都不是问题。
这段代码真的是一个问题,我是否需要添加一些虚拟返回语句?
这个函数的分支是否会导致无返回语句?
这是g ++静态分析器的缺点。它没有得到所有枚举值在switch语句中正确处理的事实。
您可以在这里注意https://godbolt.org/z/LQnBNi clang不会对其当前形状的代码发出任何警告,并发出两个警告(“并非所有枚举值都在switch中处理”和“控件在非void函数上达到结束”)另一个值被添加到枚举。
请记住,编译器诊断没有以任何方式标准化 - 编译器可以自由地报告符合代码的警告,并报告错误程序的警告(和编译!)。
你必须要记住,在C ++中,enum
s不是它们的样子。它们只是具有一些约束的int
s,并且可以很容易地假设除这些之外的其他值。考虑这个例子:
#include <iostream>
enum class MyEnum {
A = 1,
B = 2
};
int main() {
MyEnum m {}; // initialized to 0
switch(m) {
case MyEnum::A: return 0;
case MyEnum::B: return 0;
}
std::cout << "skipped all cases!" << std::endl;
}
解决这个问题的方法是将default
用assert(false)
作为VTT指示above,或者(如果你能给每个人保证指定集合之外的任何值都不会到达那里)使用编译器特定的提示,如GCC上的__builtin_unreachable()
和铿锵:
switch(m) {
case MyEnum::A: return 0;
case MyEnum::B: return 0;
default: __builtin_unreachable();
}
首先,您描述的是警告,而不是错误消息。编译器不需要发出此类警告,并且仍然允许成功编译代码 - 因为它在技术上是有效的。
实际上,大多数现代编译器都会发出此类警告,但在默认配置中却没有。使用gcc,可以选择将编译器配置为发出此类警告(例如,使用合适的命令行选项)。
这是linux下“永远不会出问题”的唯一原因是因为您选择的编译器未配置(或使用合适的命令行选项)来发出警告。
大多数编译器直接(在解析源代码期间)或通过分析该代码的一些内部表示来对代码进行广泛的分析。需要进行分析以确定代码是否具有可诊断的错误,以确定如何优化性能。
由于这种分析,大多数编译器可以并且确实检测到可能存在问题的情况,即使代码没有可诊断的错误(即,它“足够正确”,C ++标准不需要诊断)。
在这种情况下,编译器可能会得出许多明显的结论,具体取决于它如何进行分析。
switch
。原则上,可以执行switch
语句之后的代码。return
的情况下到达函数的末尾,并且函数返回一个值。结果是潜在的未定义行为。如果编译器的分析得到这么多(并且编译器配置为警告这些事情),则满足发出警告的标准。如果可以抑制警告,则需要进一步分析,例如,确定e
的所有可能值都由case
表示,并且所有情况都有return
语句。问题是,编译器供应商可能出于各种原因选择不进行此类分析,因此不会禁止警告。
在这两种情况中的任何一种情况下,都不会进行分析以确定可以抑制警告,因此不会抑制警告。编译器根本没有做足够的分析来确定通过该函数的所有执行路径都遇到return
语句。
最后,您需要将编译器警告视为潜在问题的标志,然后对潜在问题是否值得关注做出明智的决定。您在此处的选项包括禁止警告(例如,使用导致警告被禁止的命令行选项),修改代码以防止警告(例如,在返回的return
中添加switch
和/或default
后的switch
) 。
在省略return语句时应该非常小心。这是一个未定义的行为:
9.6.3退货声明[stmt.return]
在构造函数,析构函数或具有cv void返回类型的函数的末尾流动,相当于没有操作数的返回。否则,流出除main(6.6.1)以外的函数的末尾会导致未定义的行为。
可能很容易认为这个代码很好,因为所有有效的枚举器值(在这种情况下在0..1
[0..(2 ^ M - 1)]
范围内与M = 1
)都在switch
中处理,但编译器不需要执行任何特定的可达性分析来在跳跃之前弄清楚这一点进入UB区。
此外,来自SergeyA's answer的例子表明这种代码是一个直接的定时炸弹:
class myClass {
protected:
enum class myEnum{
a,
b,
c
};
int fun(myClass::myEnum e);
};
int myClass::fun(myClass::myEnum e) {
switch (e){
case myEnum::a:{
return 0;
}
case myEnum::b:{
return 1;
}
case myEnum::c:{
return 2;
}
}
}
只需添加第三个枚举成员(并在switch
中处理它),有效枚举器值的范围就会扩展到0..3
([0..(2 ^ M - 1)]
和M = 2
)和clang happily accepts it而没有任何抱怨,即使将3传递给此函数也会错过开关,因为编译器不是必需的报告UB。
因此,经验法则是以所有路径以return
throw
或[[noreturn]]
函数结束的方式编写代码。在这个特殊情况下,我可能会为未处理的枚举器值写一个带有assertion
的return语句:
int myClass::fun(myClass::myEnum e) {
int result{};
switch (e){
case myEnum::a:{
result = 0;
break;
}
case myEnum::b:{
result = 1;
break;
}
default:
{
assert(false);
break;
}
}
return result;
}