为什么Clang / LLVM在覆盖所有枚举情况的switch语句中警告我使用default?


34

考虑以下枚举和switch语句:

typedef enum {
    MaskValueUno,
    MaskValueDos
} testingMask;

void myFunction(testingMask theMask) {
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
};

我是一个Objective-C程序员,但是我已经用纯C语言编写了此文档,以供更广泛的读者使用。

带有-Weverything的Clang / LLVM 4.1在默认行警告我:

开关中的默认标签,涵盖所有枚举值

现在,我可以看到为什么会出现这种情况:在理想情况下,参数中唯一输入的值theMask将在枚举中,因此不需要默认值。但是,如果出现一些hack并将未初始化的int放入我的漂亮函数中怎么办?我的功能将作为库中的一部分提供,我无法控制其中的内容。使用default是处理此问题的一种非常简洁的方法。

为什么LLVM众神认为这种行为不符合他们的地狱装置?我应该在前面加上if语句来检查参数吗?


1
我应该说,我做任何事情的原因都是为了使自己成为一个更好的程序员。正如NSHipster所说的"Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode."
Swizzlr 2012年

2
-Weverything可能很有用,但请注意不要过多修改代码以应对它。其中一些警告不仅毫无价值,而且适得其反,最好将其关闭。(实际上,这是用例-Weverything:先将其打开,然后关闭没有意义的部分。)
史蒂芬·费舍尔

您能否在后代的警告消息中多说几句?对他们来说通常不止这些。
史蒂芬·费舍尔

阅读答案后,我仍然更喜欢您的初始解决方案。使用默认语句恕我直言比答案中给出的替代方法更好。只是一小部分:答案确实非常有用且内容丰富,它们显示了解决问题的好方法。
bbaja42

@StevenFisher这就是整个警告。正如Killian指出的,如果以后修改我的枚举,将为有效值传递到默认实现打开可能性。这似乎是一个很好的设计模式(如果是这样的话)。
Swizzlr 2012年

Answers:


31

这是一个版本,既不受问题叮当的报告困扰,也不受您的防范:

void myFunction(testingMask theMask) {
    assert(theMask == MaskValueUno || theMask == MaskValueDos);
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
}

Killian已经解释了clang发出警告的原因:如果扩展枚举,则会陷入默认情况,这可能不是您想要的。正确的做法是删除默认情况并获取未处理情况的警告。

现在,您担心有人会使用枚举之外的值来调用您的函数。听起来好像没有满足函数的先决条件:有记录表明期望testingMask枚举有一个值,但是程序员通过了其他操作。因此,请使用(或如您所说的使用Objective-C)使程序员出错。如果程序员做错了,则显示一条消息,说明程序员做错了,使您的程序崩溃。assert()NSCAssert()


6
+1。作为一个小的修改,我更喜欢每种情况return;assert(false);在末尾添加一个(而不是通过在首字母assert()和中列出合法枚举来重复自己switch)。
乔什·凯利

1
如果不正确的枚举不是由笨拙的程序员传递而是由内存损坏错误传递的,该怎么办?当驾驶员按下Toyota的制动开关,并且一个错误破坏了枚举时,中断处理程序应该崩溃并燃烧,并且驾驶员应该看到一个文本:“程序员做错了,糟糕的程序员! ”。在用户驶过悬崖边缘之前,我不太了解这对用户有什么帮助。

2
@Lundin是OP正在做什么,还是您刚刚构建的毫无意义的理论案例?无论如何,“内存损坏错误” [i]是程序员错误,[ii]并不是您可以以有意义的方式继续的事情(至少不能满足问题中所述的要求)。

1
@GrahamLee我只是说当发现意外错误时,“放下并死亡”并不一定是最佳选择。

1
它有一个新问题,您可能会让断言和案例意外地不同步。
user253751 '16

9

default这里贴上标签可以表明您对期望值感到困惑。由于您已enum明确用尽所有可能的值,因此default可能无法执行,并且也不需要它来防止将来的更改,因为如果扩展enum,则该构造将已经生成警告。

因此,编译器注意到您已经涵盖了所有基础,但似乎在您没有,这始终是一个不好的信号。通过最小的努力将更switch改为期望的形式,您可以向编译器证明您似乎在做的就是您实际在做的事情,并且您知道这一点。


1
但是我关心的是一个脏变量,并且要防止该变量(例如,就像我说的那样,是一个未初始化的int)。在我看来,在这种情况下switch语句可能会达到默认值。
Swizzlr 2012年

我从本质上不知道Objective-C的定义,但我假设编译器会强制执行typdef枚举。换句话说,“未初始化的”值只能在您的程序已经表现出未定义的行为的情况下输入该方法,并且我认为编译器没有考虑到这一点是完全合理的。
Kilian Foth 2012年

3
@KilianFoth不,不是。Objective-C枚举是C枚举,而不是Java枚举。从下面的积分型的任何值可以存在于函数参数。

2
同样,可以在运行时从整数中设置枚举。因此,它们可以包含任何可以想象的值。

OP是正确的。testingMask m; myFunction(m); 很有可能会击中默认情况。
马修·詹姆斯·布里格斯

4

Clang感到困惑,有一个默认语句,这是一种很好的实践,它被称为防御性编程,被认为是良好的编程实践(1)。它在关键任务系统中被大量使用,尽管在桌面编程中可能不使用。

防御性编程的目的是捕获理论上永远不会发生的意外错误。这样的意外错误不一定是程序员为函数提供了不正确的输入,甚至是“恶意破解”。更有可能的原因是变量损坏:缓冲区溢出,堆栈溢出,代码失控以及与您的功能无关的类似错误可能是导致这种情况的原因。对于嵌入式系统,变量可能会因EMI而改变,尤其是在使用外部RAM电路的情况下。

至于在默认语句中写些什么……如果您怀疑程序一旦结束就已经麻烦了,那么您需要某种错误处理。在许多情况下,您可能只需要添加一个带有注释的空语句即可:“出乎意料,但无关紧要”等,以表明您已经考虑到了不太可能的情况。


(1)MISRA-C:2004 15.3。


1
请不要因为一般的PC程序员通常会发现防御性编程的概念完全陌生,因为他们对程序的看法是抽象的乌托邦,在理论上没有什么会在实践中出错。因此,根据您的询问对象,您将获得非常多样的答案。

3
lang语不混淆;明确要求提供所有警告,包括不总是有用的警告,例如样式警告。(我之所以选择此警告,是因为我不希望在枚举开关中使用默认值;拥有它们意味着如果我添加枚举值并且不处理它,则不会收到警告。因此,我始终会处理枚举之外的值不值的情况。)
Jens Ayton

2
@JensAyton很困惑。如果switch语句缺少默认语句,则所有专业的静态分析器工具以及所有符合MISRA的编译器都会发出警告。自己尝试。

4
编码MISRA-C并不是C编译器的唯一有效用法,并且再次-Weverything打开每个警告,而不是适合特定用例的选择。不符合自己的口味与困惑不是一回事。
詹斯·艾顿

2
@Lundin:我敢肯定,坚持使用MISRA-C不会神奇地使程序安全和无错误...而且我同样确定,即使从未有过的人也有很多安全,无错误的C代码听说 MISRA。实际上,它只不过是某人(受欢迎但并非毫无争议)的观点,并且有些人发现其某些规则在设计的上下文之外是任意的,有时甚至是有害的。
cHao 2012年

4

更好的是:

typedef enum {
    MaskValueUno,
    MaskValueDos,

    MaskValue_count
} testingMask;

void myFunction(testingMask theMask) {
    assert(theMask >= 0 && theMask<MaskValue_count);
    switch theMask {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
};

将项目添加到枚举时,这种方法不太容易出错。如果您将枚举值设为无符号,则可以跳过> = 0的测试。此方法仅在枚举值之间没有空格的情况下才有效,但通常是这种情况。


2
这会使用您根本不希望该类型包含的值来污染类型。它与此处的旧WTF有相似之处:thedailywtf.com/articles/What_Is_Truth_0x3f_
Martin Sugioarto

4

但是,如果出现一些hack并将未初始化的int放入我的漂亮函数中怎么办?

然后,您将获得Undefined Behavior,这default将毫无意义。您没有任何办法可以做得更好。

让我更加清楚。有人将未初始化int的函数传递给您的函数即为未定义行为。您的函数可以解决停止问题,没关系。是UB。UB被调用后,您将无能为力。


3
如果静默忽略它,如何记录它或抛出异常呢?
jgauffin 2012年

3
确实,有很多事情可以做,例如...向交换机添加默认语句并优雅地处理错误。这就是所谓的防御性编程。它不仅可以防止哑巴程序员移交功能不正确的输入,而且还针对引起缓冲区溢出各种错误,堆栈溢出,失控代码等等等等

1
不,它不会处理任何事情。是UB。进入功能后,程序将具有UB,功能主体对此无能为力。
DeadMG

2
@DeadMG枚举可以具有实现中其对应整数类型可能具有的任何值。没有任何未定义的东西。访问未初始化的自动变量的内容的确是UB,但是“恶意破解”(很可能是某种错误)也可能向该函数抛出一个明确定义的值,即使它不是值之一在枚举声明中列出。

2

默认语句不一定会有所帮助。如果切换通过枚举,则该枚举中未定义的任何值最终都将执行未定义的行为。

就您所知,编译器可以将该开关(默认情况下)编译为:

if (theMask == MaskValueUno)
  // Execute something MaskValueUno code
else // theMask == MaskValueDos
  // Execute MaskValueDos code

一旦触发未定义的行为,就无法返回。


2
首先,它是一个未指定的值,而不是未定义的行为。这意味着编译器可能会采用其他更方便的值,但可能不会使月亮变成绿色的奶酪,等等。其次,据我所知,这仅适用于C ++。在C语言中,enum类型的值范围与其基础整数类型的值范围相同(这是实现定义的,因此必须是自洽的)。
延斯·艾顿

1
但是,枚举变量和枚举常量的实例不一定需要具有相同的宽度和符号,它们都是隐式定义的。但是我很确定C中的所有枚举的计算方式都与其对应的整数类型完全相同,因此那里没有未定义甚至未指定的行为。

2

我也更喜欢default:在所有情况下都有一个。我像往常一样迟到了聚会,但是...上面没有出现的其他一些想法:

  • 特定警告(或错误,如果也抛出-Werror)来自-Wcovered-switch-default(来自-Weverything而不是-Wall)。如果您的道德灵活性允许您关闭某些警告(例如,从-Wall或中删除了一些东西-Weverything),请考虑抛出-Wno-covered-switch-default(或-Wno-error=covered-switch-default使用时-Werror),通常-Wno-...对于其他警告您会感到不愉快。
  • 对于gcc(和更通用的行为clang),请参阅gcc联机帮助页-Wswitch-Wswitch-enum-Wswitch-default的(不同的)在switch语句中的枚举类型的类似情况的行为。
  • 我也从概念上不喜欢这个警告,也不喜欢它的措辞。对我而言,警告中的字样(“默认标签...覆盖所有...值”)表明该default:案例将始终执行,例如

    switch (foo) {
      case 1:
        do_something(); 
        //note the lack of break (etc.) here!
      default:
        do_default();
    }

    一读,这就是我想你正在运行到-你的default:,因为没有的情况下总是被执行break;return;或相似。这个概念类似于(在我的耳边)类似于从中产生的其他保姆风格的注释(尽管偶尔有帮助)clang。如果为foo == 1,则将执行两个函数;否则为。您上面的代码具有此行为。即,仅当您希望继续执行后续情况下的代码时,才可以突破!但是,这似乎不是您的问题。

冒充学究的风险,还有一些其他关于完整性的想法:

  • 但是,我确实认为此行为(更多)与其他语言或编译器中的主动类型检查一致。如果按照您的假设,某些重试确实尝试向int此函数传递或东西,而该函数明确打算使用您自己的特定类型,则在这种情况下,编译器应使用激进的警告或错误来同样保护您。但事实并非如此!(也就是说,似乎至少gcc并且clang不进行enum类型检查,但我听说这样icc做了)。由于您没有获得类型安全性,因此可以得到如上所述的价值安全性。否则,按照TFA中的建议,考虑struct可以提供类型安全的或。
  • 另一种解决方法可以创造你的新的“价值” enumMaskValueIllegal,而不是支持了caseswitch。这将被吃掉default:(除了任何其他古怪值)

防御编码万岁!


1

这里有一个替代建议:
在OP正试图在那里有人经过在防范的情况下int,其中一个枚举的预期。或者,更有可能的是,有人使用更多情况下的较新标头将旧库与较新程序链接在一起。

为什么不更改开关以处理这种int情况?在开关中的值前面添加强制类型转换可以消除警告,甚至可以提供有关默认值存在原因的某种程度的提示。

void myFunction(testingMask theMask) {
    int maskValue = int(theMask);
    switch(maskValue) {
        case MaskValueUno: {} // deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
}

我觉得这比少反感assert()每一个可能值的测试,甚至会假设范围枚举值是秩序井然,这样一个简单的测试工作。这只是默认值精确而精美地做的丑陋方式。


1
这篇文章很难阅读(文字墙)。您介意将其编辑为更好的形状吗?
t 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.