【问题标题】:Using default in a switch statement when switching over an enum切换枚举时在 switch 语句中使用默认值
【发布时间】:2010-02-04 16:51:49
【问题描述】:

在切换每个枚举都被一个案例覆盖的枚举时,您的程序是什么?理想情况下,您希望代码能够面向未来,您如何做到这一点?

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

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) 添加默认情况;
  • 添加引发可捕获异常的默认情况
  • 添加一个引发不可捕获异常的默认情况(可能只是从不捕获它或总是重新抛出的策略)。
  • 我没有考虑过的更好的东西

我对你为什么选择以他们的方式做这件事特别感兴趣。

【问题讨论】:

    标签: c++ enums switch-statement


    【解决方案1】:

    我抛出一个异常。确实鸡蛋就是鸡蛋,有人会将一个具有错误值而不是枚举值的整数传递给您的开关,最好是大声失败,但让程序有可能处理错误,而 assert() 不会。

    【讨论】:

    • 或者有人会添加一个新的枚举常量。
    • 您会抛出 std::exception(或其派生类之一)还是一些特殊的东西?您是否有针对“永远不会发生”错误的异常类?
    • @Caspin 我所有的库和应用程序都会抛出一个从 std::exception 派生的异常,该异常是特定于库或应用程序的——但就我而言。我非常反对创建复杂的异常层次结构。
    • 似乎 std::domain_error 会是一个不错的选择
    • @Johannes 谢谢,但这只是因为你似乎已经严重减少了在这里发帖的次数——恕我直言,这对我们所有人来说都是一个可悲的损失
    【解决方案2】:

    我会输入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 永远不会返回。在我看来,我们立即终止失败的程序,这是唯一合理的反应(试图继续运行导致未定义行为的程序是没有意义的)。

    【讨论】:

    • 能否优雅地处理取决于应用程序。考虑一个多线程 Web 服务器 - 如果一个线程使用无效的枚举值,这是终止整个服务器的理由吗?有些人可能会说“是”,但我更愿意记录错误,终止导致它的线程并继续。为了实现这一点,我会抛出一个异常,而不是调用 assert()。
    • @Caspin,你不能在 C++ 中抛出不可捕获的异常。任何人都可以被catch(...) 抓住。在这种情况下,是的,我会尝试在开发过程中找到错误。 @Neil,我肯定更愿意终止整个应用程序并修复错误。线程共享相同的地址空间,一旦一个线程变得疯狂并且超出了程序员在编写代码时所假设的范围,我们就不能再谈论程序中任何东西的状态了。说“哦,我希望程序至少在剩余的线程中继续正确”是没有帮助的。
    • @Johannes - VS2008 不将“中止”识别为终止路径。不过,exit(0) 完成了这项工作。
    • @Neil,我已经向 Eric Lippert 提出了这个问题。请参阅此问题的 cmets:stackoverflow.com/questions/990115/…:“无法抛出的异常是死代码,不可测试,应该被消除。所以你去:如果可能出现意外错误,则抛出异常。如果不可能然后用一个断言记录这个事实,这样你就会被告知你的不可能的假设是否不正确。”
    • +1,断言是正确的方法。我更喜欢使用BOOST_ASSERT。然后,如果您决定在绝对不能终止的服务器中使用您的库,您可以定义 boost::assertion_failed 函数并从那里抛出异常。
    【解决方案3】:

    首先,我会总是switch 声明中使用default。即使周围没有白痴将integers 转换为enums,default 可以帮助捕获内存损坏的可能性。值得一提的是,MISRA 规则要求存在默认值。

    关于你做什么,这取决于情况。如果可以很好地处理异常,请处理它。如果它是代码的非关键部分中的状态变量,请考虑将状态变量静默重置为初始状态并继续(可能记录错误以供将来参考)。如果它会导致整个程序以一种非常混乱的方式崩溃,请尝试优雅地摔倒或其他方式。简而言之,这完全取决于您switching 的内容以及错误值的严重程度。

    【讨论】:

    • MISRA?这是你诉诸权威?总是有一个默认值是一个好习惯,是的。你不需要诉诸权威来表达这一点。
    • @jmecchiello:MISRA 被公认为是经过深思熟虑的 c++ 标准。艾尔只是说这不仅仅是他的意见。如果权威是虚假的,或者可能不合格,那么“诉诸权威”只是一种谬误。
    【解决方案4】:

    作为补充说明(除了其他回复)我想指出,即使在具有相对严格的类型安全限制的 C++ 语言中(至少与 C 相比),也可以生成 enum 的值在一般情况下可能不匹配任何枚举器的类型,而不使用任何“hacks”。

    如果你有一个枚举类型E,你可以合法地这样做

    E e = E();
    

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

    换句话说,对于任何枚举类型E,表达式E() 是格式良好的,并且无论E 是如何定义的,都会生成E 类型的零值。

    请注意,这个漏洞允许人们在不使用任何“黑客”的情况下创建一个潜在的“意外”枚举值,例如将int 值强制转换为您在问题中提到的枚举类型。

    【讨论】:

      【解决方案5】:

      你的东西很好。但我会删除“抛出可捕获的异常”。

      补充:

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

      【讨论】:

        【解决方案6】:

        作为进一步的选择:避免切换枚举。

        【讨论】:

        • 很棒的答案。你会建议什么?
        • 视情况而定。多态性、查找表?
        • 我不知道为什么这被否决了。它当然不是在所有情况下都适用,但是在适当的情况下,多态性可以使这成为非问题。在问题的第二个示例中,您可以使用 do() 方法创建一个类 1 和一个类 2,每个类都派生自一个抽象类基础。然后不是传入 e,而是传入一个 base* b,并调用 b->do()。一个优点是,如果添加了另一个选项,则无需编辑开关,只需添加新的派生类即可。就像我说的那样,这不是一个通用的解决方案,但它确实有效,它运作良好。
        • UncleBens 应该在他的答案中加入他的替代方案,但 Keith 是正确的。 UncleBens 不应该被否决,因为这是一个非常好的建议。
        【解决方案7】:

        LLVM 编码标准的意见是:Don’t use default labels in fully covered switches over enumerations

        他们的理由是:

        -Wswitch 会在没有默认标签的枚举上的开关不覆盖每个枚举值时发出警告。如果你在枚举的完全覆盖的开关上写了一个默认标签,那么当新元素添加到该枚举时,-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;
        }
        

        如果您在开关的每个情况下都选择return,则不需要boost::optional:只需在switch 块之后无条件地调用std::abort()throw

        但重要的是要记住,也许switch 并不是最好的设计工具(正如@UncleBens answer 中所述):多态性或某种类型的查找表可以提供更好的解决方案,特别是如果你的枚举有很多元素。

        PS:出于好奇,Google C++ Style Guideswitchs-over-enums 做了一个例外,因为没有default 案例:

        如果不是以枚举值为条件,switch 语句应始终具有默认情况

        【讨论】:

          【解决方案8】:

          我倾向于选择选项 2:

          添加一个引发可捕获异常的默认情况

          如果问题发生的话,它应该会突出显示问题,并且只花费你几行代码来实现。

          【讨论】:

            【解决方案9】:

            断言然后可能抛出。

            对于与此相同项目中的内部代码(您没有说函数边界是什么 - 内部库、外部库、内部模块...),它将在开发期间断言。这就是你想要的。

            如果代码是供公众使用的(其他团队,出售等),那么断言将消失,而您只剩下 throw。这对外部消费者更有礼貌

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

            【讨论】:

              【解决方案10】:

              我的 0.02 美元:

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

              如果此方法在您的代码内部(只有您调用它),那么断言应该是所有必要的。这将捕获某天添加新枚举值并忘记更新您的 switch 语句的情况。

              总是在每个开关中提供一个默认情况,至少,如果它被击中,则断言。这个习惯会为您节省数小时甚至数天的时间,从而获得回报。

              【讨论】:

                【解决方案11】:

                除了建议抛出异常,如果你使用 gcc,你可以使用 -Wswitch-enum (最后是 -Werror=switch -enum) 如果您的枚举成员在任何情况下都没有出现,它将添加警告(或错误)。 其他编译器可能有等价物,但我只在 gcc 上使用它。

                【讨论】:

                  【解决方案12】:

                  在与提供的示例类似的情况下,您实际上可以将选项 1 与其他选项之一结合起来: 省略默认值并启用适当的编译器警告(如果可用)。这样一来,您可以立即发现是否添加了新的枚举值,并且可以方便地添加案例,而不必保证在运行时执行特定的代码路径来找到它。

                  然后在 switch 结束和函数结束之间添加代码来断言或抛出(我更喜欢断言,因为这确实是一种无效的情况)。这样,如果有人将 int 强制转换为您的枚举类型,您仍然会得到运行时检查。

                  【讨论】:

                    【解决方案13】:

                    我不是 C++ 人。但是,在 C# 中,我应该这样写

                    enum Enum
                    {
                        Enum_One,
                        Enum_Two
                    };
                    
                    
                    Special make_special( Enum e )
                    {
                    
                        if(Enums.IsDefined(typeof(Enum),(int)e))
                        {
                           switch( e )
                           {
                              case Enum_One:
                                  return Special( /*stuff one*/ );
                              case Enum_Two:
                                  return Special( /*stuff two*/ );
                           }
                        }
                        // your own exception can come here.
                        throw new ArgumentOutOfRangeException("Emum Value out of range");
                    
                    }
                    

                    【讨论】:

                    • 这与使用默认情况下的 throw 有何不同?在正常情况下,它会检查 e 的值是否良好,然后将其通过开关,从而对其进行两次评估。怎么样更好?并假设 Enum 定义了 Enum_Three 的值。那仍然会被抛出超出范围。这是故意的吗?
                    • @Sumit:对于那些担心性能问题的人,Enums.IsDefined 使用反射。
                    【解决方案14】:

                    我个人推荐您的任何解决方案,除了第一个。保留默认情况,断言,抛出(你喜欢的任何异常类型)。在 C# 中,如果您忽略默认大小写,Visual Studio 不会发出警告。

                    我建议你添加案例并失败的原因是这是代码维护的一个重要点。一旦有人添加到枚举列表中,switch 语句就必须增长以匹配。

                    另外(正如 UncleBen 所说),看看您是否可以通过使用多态性来针对整个场景进行设计。这样,当您向枚举添加新值时,您只需将其添加到一个位置。任何时候你看到一个枚举的开关,你都应该考虑使用多态性。

                    【讨论】:

                      【解决方案15】:

                      除了在运行时首选解决方案的异常(并将处理疯狂的强制转换),我也倾向于使用静态编译时断言。

                      您可以执行以下操作:

                      //this fails at compile time when the parameter to the template is false at compile time, Boost should provide a similar facility
                      template <COLboolean IsFalse> struct STATIC_ASSERTION_FAILURE;
                      template <> struct STATIC_ASSERTION_FAILURE<true>{};
                      #define STATIC_ASSERT( CONDITION_ ) sizeof( STATIC_ASSERTION_FAILURE< (COLboolean)(CONDITION_) > );
                      
                      //
                      // this define will break at compile time at locations where a switch is being
                      // made for the enum. This helps when adding enums
                      //
                      // the max number here should be updated when a new enum is added.
                      //
                      // When a new enum is added, at the point of the switch (where this
                      // define is being used), update the switch statement, then update
                      // the value being passed into the define.
                      //
                      #define ENUM_SWITCH_ASSERT( _MAX_VALUE )\
                         STATIC_ASSERT( _MAX_VALUE  ==  Enum_Two)
                      
                      enum Enum
                      {
                          Enum_One = 0,
                          Enum_Two = 1
                      };
                      

                      然后在您的代码中,每当您使用枚举集时:

                      ENUM_SWITCH_ASSERT( Enum_Two )
                      switch( e )
                      {
                          case Enum_One:
                              do_one();
                              break;
                          case Enum_Two:
                              do_two();
                              break;
                      }
                      

                      现在,每当您更改宏 ENUM_SWITCH_ASSERT 以处理新的枚举值时,它都会在编译时在使用枚举集的位置附近中断。在添加新案例时很有帮助。

                      【讨论】:

                        【解决方案16】:

                        因为了解枚举的意外值可能会有所帮助,所以请编写您自己的 BADENUM(Enum) 宏,如下所示:

                        #define _STRIZE(x) _VAL(x)
                        #define _VAL(x) #x
                        extern void  __attribute__ ((noreturn)) AssertFail(const char *Message);
                        #define ASSERT(Test) ((Test) ? (void)0 : AssertFail(__FILE__ ":" _STRIZE(__LINE__) " " #Test))
                        
                        extern void  __attribute__ ((noreturn)) BadEnum(const char *Message, const long unsigned Enum);
                        #define BADENUM(Enum)  BadEnum(__FILE__ ":" _STRIZE(__LINE__), (u32)Enum))
                        

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 2011-03-02
                          • 2012-04-30
                          • 1970-01-01
                          • 2017-12-13
                          • 1970-01-01
                          相关资源
                          最近更新 更多