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

5
我正在LINUX上使用一个较旧的gcc版本(如果我没记错,是7.x)来开发应用程序。最近我尝试在Windows上运行同一应用程序。在Windows上,我使用MinGW作为编译器(带有gcc 8.1.0)。
在编译我的应用程序时,我遇到了以下错误消息:
“warning: control reaches end of non-void function [-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 上从未出现过此问题。

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

是否有一个分支函数不需要返回语句?


3
为什么不直接添加 default: std::terminate(); 或类似的内容?如果您的假设是 default: 分支永远不会被执行,那么就不会有更多警告和伤害。而且,如果你最终证明是错误的(有人扩展了 myEnum 或者不正确地将其转换?),那么您就可以捕获一个本应该导致 UB 的恶意错误。 - François Andrieux
2
编译器可以发出任何它们喜欢的“警告”消息。而且,我会说代码是一个问题 - 如果您更新枚举值但不更新函数,会发生什么? - user2100815
4
如果你更新了枚举值但没有更新函数,会发生什么?嗯,在这种情况下,源代码必须重新编译,然后编译器可以发出警告。 - Slava
2
考虑 fun(static_cast<myClass::myEnum>(2)) - melpomene
3
@Slava 这不一定是未定义行为:https://dev59.com/SGMl5IYBdhLWcg3wsIyA - François Andrieux
显示剩余17条评论
4个回答

4

在C++中,你必须记住enum并不是看起来那样。它们只是带有一些限制的int,可以轻松地假定其他值而不是所示的值。考虑以下例子:

#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在上述所示,要么(如果您可以保证来自指定集合之外的任何值都不会到达)使用编译器特定的提示,例如在GCC和clang上使用__builtin_unreachable()
  switch(m) {
    case MyEnum::A: return 0;
    case MyEnum::B: return 0;
    default: __builtin_unreachable();
  }

4
这是g++静态分析器的一个缺点。它没有意识到在switch语句中处理了所有枚举值的事实。
您可以在这里注意到https://godbolt.org/z/LQnBNi,clang对其当前形式的代码没有发出任何警告,并在向枚举添加另一个值时发出两个警告(“并非所有枚举值都在switch语句中处理”和“控制达到非void函数的结尾”)。
请记住,编译器诊断并没有标准化 - 编译器可以自由地为符合规范的代码报告警告,并为格式错误的程序报告警告(并编译!)。

应该是“报告没有警告……”吗?你漏掉了“没有”。 - Slava
@Slava,不是 - 我的意思就是这样,编译器允许报告符合标准的程序的警告,这在标准中并没有禁止。 - SergeyA
那么结论是,只要我不向枚举添加新元素,我的代码就应该没问题了? - user7431005
2
@user7431005 是的,如果你向枚举中添加新值,你将会收到另一个警告,指出在switch语句中没有处理所有的枚举值。你可以像其他答案中所述使用__builtin_unreachable()来消除这个警告。 - SergeyA
1
我实际上发现警告“switch语句未处理所有枚举值”更加烦人和无用,因为很多时候我是故意这么做的。所以奇怪的是gcc可以检测到是否处理了所有枚举值来产生这个无用的警告,但不能用它来抑制不可达的返回值。 - Slava
显示剩余2条评论

1
首先,你所描述的是一个警告,而不是错误信息。编译器没有义务发布这样的警告,仍然可以成功编译你的代码——因为它在技术上是有效的。
实际上,大多数现代编译器都可以发出这样的警告,但在默认配置下不会这样做。使用gcc编译器时,可以选择配置以发出这样的警告(例如使用适当的命令行选项)。
这在Linux下从未成为问题的唯一原因是你选择的编译器没有被配置(或者没有使用适当的命令行选项)以发出警告。
大多数编译器对代码进行广泛的分析,直接(在解析源代码期间)或通过分析某些内部表示来进行分析。这种分析是必要的,以确定代码是否存在可诊断的错误,并确定如何优化性能。
由于此类分析,大多数编译器可以检测到可能有问题的情况,即使代码没有可诊断的错误(即它“足够正确”,C++标准也没有要求诊断)。
在这种情况下,编译器可能会得出许多不同的结论,具体取决于它如何进行分析。
  • 有一个switch语句。原则上,switch语句后的代码可能会被执行。
  • switch语句后的代码在没有return语句的情况下到达函数结尾,并且函数返回一个值。这可能导致未定义行为。

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

  • 进行更多分析会增加编译时间。供应商在声称他们的编译器更快等方面进行竞争,因此不进行某些分析实际上有助于获得更短的编译时间;
  • 即使代码实际上是正确的,编译器供应商也可能认为最好标记潜在的问题。在提供多余警告或不警告某些事物之间做出选择时,供应商可能更喜欢提供多余警告。
在这两种情况下,分析以确定可以抑制警告将不会进行,因此警告将不会被抑制。编译器只是没有进行足够的分析来确定函数中所有执行路径是否都遇到了 return 语句。
最终,您需要将编译器警告视为潜在问题的标志,然后就是否值得关注潜在问题做出明智的决定。您可以选择抑制警告(例如使用命令行选项来抑制警告),或修改代码以防止警告(例如在 switch 中添加一个 returndefault case 后面)。

0

在省略返回语句时,应该非常小心。这是一种未定义的行为:

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),即使将3传递到此函数中也不会有任何投诉,因为编译器不需要报告UB。

因此,经验法则是以一种使所有路径以return throw[[noreturn]]函数结尾的方式编写代码。 在这种特殊情况下,我可能会编写一个带有未处理枚举器值的assertion的单个返回语句:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接