切换枚举时在 switch 语句中使用默认值

发布于 2024-08-20 03:06:57 字数 827 浏览 2 评论 0原文

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

另外,如果某个白痴将任意 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) 添加默认情况;
  • 添加一个引发可捕获异常的默认情况
  • 添加一个引发不可捕获异常的默认情况(可能只是策略是从不捕获它或总是重新抛出)。
  • 我没有考虑过更好的事情,

我特别感兴趣的是为什么你选择按照你的方式去做。

What is your procedure when switching over an enum where every enumeration is covered by a case? Ideally you'd like the code to be future proof, how do you do that?

Also, what if some idiot casts an arbitrary int to the enum type? Should this possibility even be considered? Or should we assume that such an egregious error would be caught in code review?

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;
    }
}
  • leave off the default case, gcc will warn you (will visual studio?)
  • add a default case with a assert(false);
  • add a default case that throws a catchable exception
  • add a default case that throws a non-catchable exception (it may just be policy to never catch it or always rethrow).
  • something better I haven't considered

I'm especially interested in why you choose to do it they way you do.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(16

2024-08-27 03:06:57

我抛出一个异常。正如鸡蛋就是鸡蛋一样,有人会将一个具有错误值的整数而不是枚举值传递到您的交换机中,最好是大声失败,但让程序有可能出现错误,而assert()则不会。

I throw an exception. As sure as eggs are eggs, someone will pass an integer with a bad value rather than an enum value into your switch, and it's best to fail noisily but give the program the possibility of fielding the error, which assert() does not.

ゃ人海孤独症 2024-08-27 03:06:57

我会添加一个断言

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

I would put an 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!");
    }
}

Not handling an enumeration value, while the intention is to cover all cases, is an error in the code that needs to be fixed. The error can't be resolved from nor handled "gracefully", and should be fixed right away (so i would not throw). For having the compiler be quiet about "returning no value" warnings, call abort, like so

#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!");
    }
}

No warning by the compiler anymore about a return without value, because it knows abort never returns. We immediately terminate the failing program, which is the only reasonable reaction, in my opinion (there is no sense in trying to continue to run a program that caused undefined behavior).

明天过后 2024-08-27 03:06:57

首先,我在 switch 语句中总是有一个default。即使没有白痴将 integers 转换为 enum,也总是存在内存损坏的可能性,而 default 可以帮助捕获。无论如何,MISRA 规则将默认值的存在作为一项要求。

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

First of all, I would always have a default in a switch statement. Even if there are no idiots around to cast integers to enums, there's always the possibility of memory corruption that the default can help to catch. For what it's worth, the MISRA rules make the presence of a default a requirement.

Regarding what you do, that depends on the situation. If an exception can be handled in a good way, handle it. If it's a state variable in a non-critical part of code, consider silently resetting the state variable to the initial state and carrying on (possibly logging the error for future reference). If it is going to cause the entire program to crash in a really messy way, try to fall over gracefully or something. In short, it all depends on what you're switching on and how bad an incorrect value would be.

把回忆走一遍 2024-08-27 03:06:57

作为附加说明(除了其他响应之外),我想指出,即使在具有相对严格的类型安全限制(至少与​​ C 相比)的 C++ 语言中,也可以生成一个枚举类型的值,该值在如果不使用任何“黑客”,一般情况可能与任何枚举器都不匹配。

如果您有枚举类型 E,则可以合法地执行此操作,

E e = E();

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

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

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

As an additional remark (in addition to other responses) I'd like to note that even in C++ language with its relatively strict type-safety restrictions (at least compared to C), it is possible to generate a value of enum type that in general case might not match any of the enumerators, without using any "hacks".

If you have a enum type E, you can legally do this

E e = E();

which will initialize e with zero value. This is perfectly legal in C++, even if the declaration of E does not include a enumeration constant that stands for 0.

In other words, for any enum type E, the expression E() is well-formed and generates zero value of type E, regardless of how E is defined.

Note, that this loophole allows one to create a potentially "unexpected" enum value without using any "hacks", like a cast of an int value to enum type you mentioned in your question.

蓝海似她心 2024-08-27 03:06:57

LLVM 编码标准的意见是: 不要在枚举的完全覆盖开关中使用默认标签

他们的理由是:

-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:只需调用std::abort() 或在 switch 块之后无条件地抛出

尽管如此,重要的是要记住,也许 switch 并不是最好的设计工具(正如 @UncleBens 答案):多态性或某种类型的查找表可以提供更好的解决方案,特别是当您的枚举有很多元素时。

PS:出于好奇,Google C++ 风格指南 switchs-over-enum 不具有 default 情况的例外情况:

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

LLVM Coding Standards' opinion is: Don’t use default labels in fully covered switches over enumerations

Their rationale is:

-Wswitch warns if a switch, without a default label, over an enumeration does not cover every enumeration value. If you write a default label on a fully covered switch over an enumeration then the -Wswitch warning won’t fire when new elements are added to that enumeration. To help avoid adding these kinds of defaults, Clang has the warning -Wcovered-switch-default which is off by default but turned on when building LLVM with a version of Clang that supports the warning.

Based on this , I personally like doing:

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

If you choose to return on every case of the switch, you don't need boost::optional: simply call std::abort() or throw unconditionally just after the switch block.

It's nonetheless important to remember that maybe switch is not the best design tool to begin with (as already stated in @UncleBens answer): Polymorphism or some type of lookup-table could place a better solution, especially if your enum has a lot of elements.

PS: As a curiosity, the Google C++ Style Guide makes an exception to switchs-over-enums not to have default cases:

If not conditional on an enumerated value, switch statements should always have a default case

ζ澈沫 2024-08-27 03:06:57

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

附加:

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

Your items are good. But I'd remove 'throw catchable exception'.

Additional:

  • Make warnings to be treated as errors.
  • Add logging for default cases.
慕烟庭风 2024-08-27 03:06:57

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

As a further option: Avoid switching over enums.

绮烟 2024-08-27 03:06:57

我倾向于选择选项2:

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

如果问题发生,它应该会突出显示问题,并且只需要几行代码即可实现。

I tend to do option 2:

add a default case that throws a catchable exception

That should highlight the problem if it ever occurs and it only costs you a couple of lines to imlpement.

七秒鱼° 2024-08-27 03:06:57

断言然后可能抛出。

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

如果代码供公众使用(其他团队、出售等),则断言将消失,您将面临抛出。这对于外部消费者来说更有礼貌

如果代码始终是内部的那么只需断言

assert and then maybe throw.

For in-house code thats in the same project as this (you didnt say what the function boundary is - internal lib, external lib, inside module,...) it will assert during dev. THis is what you want.

If the code is for public consumption (other teams, for sale etc) then the assert will disappear and you are left with throw. THis is more polite for external consumers

If the code is always internal then just assert

离笑几人歌 2024-08-27 03:06:57

我的 $0.02:

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

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

始终在每个开关中提供默认情况,并且至少在它被击中时断言。这个习惯会给你带来回报,让你节省数小时甚至数天的时间。

My $0.02:

If this method is externally visible (called by code that you don't control), then you probably need to handle the possibility of someone sending you an invalid enum value. In that case, throw an exception.

If this method is internal to your code (only you call it), then the assertion should be all that is necessary. This will catch the case of someday adding a new enum value and forgetting to update your switch statement.

Always provide a default case in every switch, and at the very least, assert if it gets hit. This habit will pay off by saving you hours or even days of head scratching every once in a while.

请远离我 2024-08-27 03:06:57

除了抛出异常的建议之外,如果您使用gcc,您还可以使用-Wswitch-enum(最终-Werror=switch-enum< /strong>) 如果您的枚举成员在任何情况下都没有出现,则会添加警告(或错误)。
其他编译器可能有等效的东西,但我只在 gcc 上使用它。

In addition to the advise to throw an exception, if you use gcc, you can use the -Wswitch-enum (and eventually -Werror=switch-enum) which will add a warning (or an error) if a member of you enum doesn't appear in any case.
There may be an equivalent for other compilers but I only use it on gcc.

半﹌身腐败 2024-08-27 03:06:57

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

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

In cases similar to the example provided you can actually combine option 1 with one of the other options:
Omit the default and enable the appropriate compiler warning if available. This way you find out immediately if a new enumerated value is added and you can conveniently add the case without having to guarantee that a specific code path executes to find it at runtime.

Then between the end of the switch and the end of the function add code to assert or throw (I prefer assert as this really is an invalid situation). This way if someone casts an int to your enum type you still get the runtime checking as well.

不爱素颜 2024-08-27 03:06:57

我不是 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");

}

I am not a C++ guy. But, in C#, I whould have written it like

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");

}
一袭水袖舞倾城 2024-08-27 03:06:57

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

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

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

I personally recommend any of your solutions, except the first. Leave in the default case, and assert, throw (whatever exception type you like). In C#, Visual Studio doesn't warn you if you leave out the default case.

The reason I suggest you add the case and fail on it is that this is an important point of code maintenance. As soon as someone adds to the enumerated list, the switch statement has to grow to match.

Also (as UncleBen said), see if you can design against this whole scenario by using polymorphism. That way, when you add a new value to the enumeration, you only have to add it in one place. Any time you see a switch on an enum, you should consider using polymorphism instead.

另类 2024-08-27 03:06:57

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

您可以执行以下操作:

//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 来处理新的枚举值时,它都会在编译时在使用枚举集的位置附近中断。添加新案例时有很多帮助。

Apart from exceptions which would be the preferred solution at runtime (and will handle crazy casting), I tend to use static compile time assertions as well.

You can do the following:

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

Then in your code, whenever you use the enum set:

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

Now whenever you change the macro ENUM_SWITCH_ASSERT to handle a new enum value, it will break at compile time near locations that use the enum set. Helps lots when adding new cases.

猫九 2024-08-27 03:06:57

因为它可能有助于了解枚举的意外值是什么,所以编写您自己的 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))

Because it may help to know what the unexpected value of the enum was, write your own BADENUM(Enum) macro, along the lines of:

#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))
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文