使用switch语句在函数中没有返回

问题描述 投票:4回答:4

我正在使用较旧的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中从来都不是问题。

这段代码真的是一个问题,我是否需要添加一些虚拟返回语句?

这个函数的分支是否会导致无返回语句?

c++ function switch-statement return
4个回答
4
投票

这是g ++静态分析器的缺点。它没有得到所有枚举值在switch语句中正确处理的事实。

您可以在这里注意https://godbolt.org/z/LQnBNi clang不会对其当前形状的代码发出任何警告,并发出两个警告(“并非所有枚举值都在switch中处理”和“控件在非void函数上达到结束”)另一个值被添加到枚举。

请记住,编译器诊断没有以任何方式标准化 - 编译器可以自由地报告符合代码的警告,并报告错误程序的警告(和编译!)。


3
投票

你必须要记住,在C ++中,enums不是它们的样子。它们只是具有一些约束的ints,并且可以很容易地假设除这些之外的其他值。考虑这个例子:

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

解决这个问题的方法是将defaultassert(false)作为VTT指示above,或者(如果你能给每个人保证指定集合之外的任何值都不会到达那里)使用编译器特定的提示,如GCC上的__builtin_unreachable()和铿锵:

  switch(m) {
    case MyEnum::A: return 0;
    case MyEnum::B: return 0;
    default: __builtin_unreachable();
  }

1
投票

首先,您描述的是警告,而不是错误消息。编译器不需要发出此类警告,并且仍然允许成功编译代码 - 因为它在技术上是有效的。

实际上,大多数现代编译器都会发出此类警告,但在默认配置中却没有。使用gcc,可以选择将编译器配置为发出此类警告(例如,使用合适的命令行选项)。

这是linux下“永远不会出问题”的唯一原因是因为您选择的编译器未配置(或使用合适的命令行选项)来发出警告。

大多数编译器直接(在解析源代码期间)或通过分析该代码的一些内部表示来对代码进行广泛的分析。需要进行分析以确定代码是否具有可诊断的错误,以确定如何优化性能。

由于这种分析,大多数编译器可以并且确实检测到可能存在问题的情况,即使代码没有可诊断的错误(即,它“足够正确”,C ++标准不需要诊断)。

在这种情况下,编译器可能会得出许多明显的结论,具体取决于它如何进行分析。

  • 有一个switch。原则上,可以执行switch语句之后的代码。
  • 切换后的代码在没有return的情况下到达函数的末尾,并且函数返回一个值。结果是潜在的未定义行为。

如果编译器的分析得到这么多(并且编译器配置为警告这些事情),则满足发出警告的标准。如果可以抑制警告,则需要进一步分析,例如,确定e的所有可能值都由case表示,并且所有情况都有return语句。问题是,编译器供应商可能出于各种原因选择不进行此类分析,因此不会禁止警告。

  • 进行更多分析会增加编译时间。除了其他方面,供应商竞争其编译器的声称更快,因此不进行某些分析因此有助于缩短编译时间;
  • 编译器供应商可能会认为标记潜在问题更好,即使代码实际上是正确的。如果在给出无关警告或不警告某些事情之间做出选择,供应商可能更愿意提供无关警告。

在这两种情况中的任何一种情况下,都不会进行分析以确定可以抑制警告,因此不会抑制警告。编译器根本没有做足够的分析来确定通过该函数的所有执行路径都遇到return语句。

最后,您需要将编译器警告视为潜在问题的标志,然后对潜在问题是否值得关注做出明智的决定。您在此处的选项包括禁止警告(例如,使用导致警告被禁止的命令行选项),修改代码以防止警告(例如,在返回的return中添加switch和/或default后的switch) 。


0
投票

在省略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;
}
© www.soinside.com 2019 - 2024. All rights reserved.