在使用枚举类型进行switch语句切换时,使用默认值(default)的方法是什么?

44

当切换一个枚举(enum)时,如果每个枚举都被case覆盖,你的过程是什么? 理想情况下,您希望代码具有未来可扩展性,如何实现?

另外,如果某些人将任意整数强制转换为枚举类型,该怎么办? 这种可能性是否应该考虑? 或者我们应该假设这样的错误会在代码审查中被发现?

enum Enum
{
    Enum_One,
    Enum_Two
};

Special make_special( Enum e )
{
    switch( e )
    {
        case Enum_One:
            return Special( /*stuff one*/ );

        case Enum_Two:
            return Special( /*stuff two*/ );
    }
}

void do_enum( Enum e )
{
    switch( e )
    {
        case Enum_One:
            do_one();
            break;

        case Enum_Two:
            do_two();
            break;
    }
}
  • 不要写默认情况,gcc会发出警告(Visual Studio呢?)
  • 添加一个默认情况,并使用 assert(false)
  • 添加一个默认情况,抛出可捕获异常;
  • 添加一个默认情况,抛出不可捕获异常(可能是策略上不允许捕获或始终重新抛出)。
  • 我没有考虑过的更好的方法。

我特别想知道您选择这样做的原因。

16个回答

36

我抛出一个异常。毫无疑问,总会有人将一个坏值的整数而不是枚举值传递到您的开关中,最好是大声失败,但给程序处理错误的可能性,这是assert()所不能做到的。


10
或者有人会添加一个新的枚举常量。 - Michael Myers
你会抛出std::exception(或其派生类)还是其他特殊异常?你是否有一个专门用于“永远不应该发生”的错误的异常类? - deft_code
2
@Caspin 我所有的库和应用程序都会抛出从特定于该库或应用程序的std::exception派生的异常 - 但这就是我的极限。我非常反对创建复杂的异常层次结构。 - anon
3
似乎在此处抛出std::domain_error异常是一个不错的选择。 - rmeador
顺便说一句,我很高兴恭喜你在有史以来的C++排行榜上获得第一名。玩得开心 :) - Johannes Schaub - litb
5
谢谢,只是因为你似乎在这里的发帖数量大幅减少了——在我看来,这是我们所有人的遗憾。 - anon

24

我会使用assert

Special make_special( Enum e )
{
    switch( e )
    {
        case Enum_One:
            return Special( /*stuff one*/ );

        case Enum_Two:
            return Special( /*stuff two*/ );

        default:
            assert(0 && "Unhandled special enum constant!");
    }
}

如果本意是涵盖所有情况,但未处理枚举值,则是代码中需要修复的错误。该错误无法从任何地方解决或“优雅”处理,应立即修复(以便不会抛出异常)。要使编译器不再显示“返回无值”警告,请调用abort

#ifndef NDEBUG
#define unreachable(MSG) \
  (assert(0 && MSG), abort())
#else
#define unreachable(MSG) \
  (std::fprintf(stderr, "UNREACHABLE executed at %s:%d\n", \
                __FILE__, __LINE__), abort())
#endif 

Special make_special( Enum e )
{
    switch( e )
    {
        case Enum_One:
            return Special( /*stuff one*/ );

        case Enum_Two:
            return Special( /*stuff two*/ );

        default:
            unreachable("Unhandled special enum constant!");
    }
}

编译器不再警告没有返回值,因为它知道abort永远不会返回。在我看来,立即终止失败的程序是唯一合理的反应(尝试继续运行导致未定义行为的程序毫无意义)。


3
能否妥善处理这个问题取决于应用程序。以多线程web服务器为例,如果一个线程使用了无效的枚举值,是否有理由终止整个服务器?有些人可能会说“是”,但我更倾向于记录错误、终止导致错误的线程并继续执行。为了达到这个目的,我会抛出一个异常而不是调用assert()。 - anon
3
@Caspin,你不能在C++中抛出不可捕获的异常。所有异常都可以通过catch(...)来捕获。在这种情况下,是的,我会在开发期间尝试找到错误。@Neil,我肯定更喜欢终止整个应用程序并修复错误。线程共享相同的地址空间,一旦线程失控并超出程序员编写代码时所假设的范围,我们就无法再对程序中的任何状态进行说明。说“噢,我希望程序至少在剩余的线程内正确运行”是没有帮助的。 - Johannes Schaub - litb
3
我已经向Eric Lippert提出了这个问题,并在此问题的评论中提到:https://dev59.com/lnNA5IYBdhLWcg3wX8nk :“无法抛出的异常是死代码,无法测试,应该被消除。因此,如果可能出现意外错误,则抛出异常。如果不可能发生,则使用断言记录该事实,以便在假设不可能时得到通知。” - Johannes Schaub - litb
2
@Neil 不用担心,我不会生气的 :) 我的理解是该函数旨在涵盖所有情况。然而,如果它旨在涵盖所有情况,那么默认情况是不可能到达的。在这种情况下,“异常情况”就不会发生:抛出异常代码将无效。代码编写者可以通过断言声明这种不可能性。 - Johannes Schaub - litb
3
+1,断言是正确的方法。我更喜欢使用BOOST_ASSERT。如果您决定在绝不能终止的服务器中使用您的库,您可以定义boost::assertion_failed函数并从那里抛出异常。 - avakar
显示剩余13条评论

9
首先,在switch语句中,我始终会使用一个default。即使没有傻瓜将int型转换为枚举类型,也有可能发生内存损坏,而default可以帮助我们捕捉这种情况。值得一提的是,MISRA规则要求必须存在default。
至于如何处理异常,这取决于具体情况。如果可以良好地处理异常,请处理它。如果它是非关键代码部分的状态变量,请考虑将状态变量静默重置为初始状态并继续执行(可能会将错误记录到日志中以备将来参考)。如果它将导致整个程序以一种非常混乱的方式崩溃,则尝试优雅地退缩或其他方式。简而言之,这都取决于你正在进行的switch操作以及错误值的影响程度。

MISRA?那是你的权威吗?始终保持默认值是一个好习惯,没错。你不需要借助权威来表达这一点。 - jmucchiello
7
MISRA被认为是C++的经过深思熟虑的标准。Al只是在说明这不仅仅是他个人的观点。只有当权威机构是虚假或可能没有资格时,才存在“权威论”的谬论。 - deft_code

7
作为额外的说明(除了其他回答),我想指出即使在C ++语言中,其相对严格的类型安全限制(至少与C相比)也可以生成一种枚举类型的值,在一般情况下可能不匹配任何枚举器,而不使用任何“技巧”。
如果您有一个枚举类型 E ,您可以合法地执行此操作。
E e = E();

这将使用零值初始化e。在C++中,即使E的声明不包括代表0的枚举常量,这也是完全合法的。

换句话说,对于任何枚举类型E,表达式E()都是良好定义的,并生成类型为E的零值,而不管E是如何定义的。

请注意,这个漏洞允许创建一个潜在的“意外”的枚举值,而无需使用任何“黑科技”,比如你在问题中提到的将int值强制转换为枚举类型。


3
LLVM编码标准的意见是:不要在完全覆盖枚举的switch语句中使用默认标签
他们的理由是:
-Wswitch警告如果一个枚举类型的switch没有默认标签并且没有涵盖每个枚举值,则会发出警告。如果在完全覆盖枚举的switch语句中写入默认标签,那么当向该枚举添加新元素时,-Wswitch警告将不会触发。为了帮助避免添加这些默认标签,Clang有一个警告-Wcovered-switch-default,默认情况下关闭,但是在使用支持该警告的Clang版本构建LLVM时会打开。
基于此,我个人喜欢做:
enum MyEnum { A, B, C };

int func( MyEnum val )
{
    boost::optional<int> result;  // Or `std::optional` from Library Fundamental TS

    switch( val )
    {
    case A: result = 10; break;
    case B: result = 20; break;
    case C: result = 30; break;
    case D: result = 40; break;
    }

    assert( result );  // May be a `throw` or any other error handling of choice

    ...  // Use `result` as seen fit

    return result;
}

如果您选择在switch的每个case中使用return,那么您就不需要使用boost::optional:只需在switch块之后无条件调用std::abort()或throw即可。
然而需要记住的是,也许switch并不是最好的设计工具(正如@UncleBens在answer中已经指出的):多态性或某种查找表可能会提供更好的解决方案,特别是当您的枚举有很多元素时。
附注:作为一件好玩的事情,Google C++风格指南 对于枚举类型的switch语句做了一个例外:不需要加上default case:

如果不基于枚举值做条件判断,则switch语句总是应该有一个default case


此外,Rust 语言采取了一种新的枚举值识别方式,即通过编译失败来提示代码中未显式考虑到它们的存在。 - Israel Shainert

2

您的项目很好。但我会移除“抛出可捕获异常”的部分。

另外:

  • 将警告视为错误处理。
  • 添加默认情况的日志记录。

2
作为另一个选项:避免切换枚举。

7
很棒的回答。你提议提出什么替代方案? - deft_code
2
取决于。多态,查找表? - UncleBens
2
我不知道为什么这个被投票否决了。虽然它并非适用于每种情况,但在适当的情况下,多态性可以使这成为一个非问题。在问题的第二个示例中,您可以创建一个类一和一个类二,每个类都派生自抽象类基类,并具有一个do()方法。然后,您可以传入一个base* b而不是e,并调用b->do()。其中一个优点是,如果添加另一个选项,则无需编辑开关,只需添加一个新的派生类即可。 就像我说的那样,这不是一个通用解决方案,但在适用的情况下,它可以很好地工作。 - KeithB
UncleBens 应该在他的回答中列出他的替代方案,但 Keith 是正确的。 UncleBens 不应该被投反对票,因为这是一个非常好的建议。 - Merlyn Morgan-Graham

1
我倾向于选择方案二:
添加一个默认情况,抛出可捕获的异常。
这样,如果问题发生,就可以准确地找到它,而且实现起来只需要几行代码。

1

断言,然后可能抛出异常。

对于与此处在同一项目中的内部代码(您没有说明函数边界-内部库、外部库、模块内部等),它将在开发过程中进行断言。这是您想要的。

如果代码供公众使用(其他团队、出售等),则断言将消失,您只能抛出异常。这对于外部用户更加礼貌。

如果代码始终为内部,则只需断言。


1

我的看法:

如果这个方法是外部可见的(被你无法控制的代码调用),那么你可能需要处理有人发送无效枚举值的可能性。在这种情况下,抛出异常。

如果这个方法只在你的代码内部使用(只有你调用它),那么断言应该就足够了。这将捕获到有一天添加了新的枚举值但忘记更新 switch 语句的情况。

在每个 switch 中始终提供默认情况,并至少断言它是否被触发。这个习惯会通过节省你偶尔几个小时甚至几天的头痛来得到回报。


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