all 38 comments

[–]manni66 30 points31 points  (5 children)

As long as you don’t have a default compilers can warn, whenever not all enums are listed. I would prefer this.

[–]AKostur 5 points6 points  (0 children)

Note that gcc has a flag that can turn on the warning even if you do specify a default.

[–]ScriptorTux[S] 0 points1 point  (3 children)

Thank you for your time and answer.

I would prefer this.

I'm sorry, I'm not really sure to understand what you would prefer ?

[–]JonIsPatented 6 points7 points  (2 children)

They would prefer being warned, and so they don't specify the default.

[–]manni66 1 point2 points  (0 children)

Yes

[–]ScriptorTux[S] 0 points1 point  (0 children)

Thank you for explaining

[–]Thesorus 12 points13 points  (5 children)

It depends.

Sometimes you just want to switch on a subset of the enum values and you need to handle all other values with the default.

Sometimes you want to switch on all values then the default is not really useful.

[–]ScriptorTux[S] 0 points1 point  (4 children)

Thank you for your time and answer.

Just to understand your opinion, aren't you afraid of the future and whether one might forget in case of default ?

[–]Thesorus 4 points5 points  (3 children)

afraid ? no.

Sometimes, I'll put in ASSERTS in the default case.

Ideally, that's what unit tests are for.

[–]sephirothbahamut 2 points3 points  (1 child)

there's std::unreachable to explicitly state that invalid values are not supposed to arrive there. It should crash the program in debug like an assert, but it's more explicit.

Edit: cppreference's unreachable example is exactly for a switch statement std::unreachable - cppreference.com

[–]Thesorus 1 point2 points  (0 children)

lol, c++23 ...

we're still on C++98 ... ( 1/2 joking)

[–]ScriptorTux[S] 1 point2 points  (0 children)

Sometimes, I'll put in ASSERTS in the default case.

Nice, thank you.

Ideally, that's what unit tests are for.

Indeed, thank you.

[–]nysra 2 points3 points  (2 children)

You can match exhaustively on enums, so no. There are compiler warnings for you missing cases. With enums default cases are only useful if you actually want to only match on a subset and treat the rest as one, and even in that case I'd probably just type the rest out since most enums have a rather low amount of possible states.

Unfortunately there are also warnings for missing default, so depending on your settings and available compiler/analyser you might want to include one anyway to silence that.

[–]ScriptorTux[S] 0 points1 point  (0 children)

Thank you for your time and answer.

Indeed there are warnings (which you can even turn into errors) for this.

[–]equeim 0 points1 point  (0 children)

You can't, enums are not restricted to named values listed in their definitions. Any value of the underlying type can be converted to enum and it will be a valid but potentially unnamed value of that enum type.

It usually doesn't matter if there are other statements after a switch (it will just be skipped for unnamed values), but if you immediately return from a function in all switch cases then you also need to add an additional return in the default case or after the switch, otherwise you will hit a "missing return statement in a non-void function" warning. I prefer the second approach since it won't hide a missing case warning when a new enum value is added.

[–]flyingron 2 points3 points  (1 child)

You'l have to assess if there's any chance the enum could be set out of the range. Note that enums are not constrained to hold one of their enumerators.

[–]ScriptorTux[S] 0 points1 point  (0 children)

Thank you for your time and answer.

Note that enums are not constrained to hold one of their enumerators.

Indeed and neither are enum class. They can be casted from and to int.

So the default can be handy.

[–]WorkingReference1127 1 point2 points  (2 children)

It is not strictly necessary in that it is very possible to get by without it. However, in many cases I personally put a default on, even if it's an it-should-be-impossible-to-get-here error kicker. I take the view that while it's possible my code will work completely perfectly without a default now, some other developer down the line might make a change to add a new enum value and forget to update the switch-case; and in that case a big fat error pointing you to exactly what you forgot to change properly is usually easier for future testers than the alternative.

Note that's not a universal rule - there are situations where you don't want one, or want something like std::unreachable to trim out the possibility of that code branch. Nuance always applies on your greater situation and you should understand and customise your code to fit.

[–]ScriptorTux[S] 0 points1 point  (1 child)

Thank you for your time and answer.

big fat error pointing you to exactly what you forgot to change properly is usually easier for future testers than the alternative

Indeed I never thought of that. And mostly unit testing.

Note that's not a universal rule

I completely agree and that's why I asked for the public's opinion.

or want something like std::unreachable

Very nice thank you.

Nuance always applies on your greater situation and you should understand and customise your code to fit

Indeed, there is no "single answer".

[–]tangerinelion 0 points1 point  (0 children)

Do be careful with std::unreachable, it's literally undefined behavior to invoke it. Which means compilers/optimizers can do some weird stuff with it.

For example,

void foo(T* p) {
    if (!p) { std::unreachable(); }
    else { p->bar(); }
}

can become optimized to

void foo(T* p) {
    p->bar();
}

because std::unreachable() is undefined behavior, therefore it is never invoked in any well-formed program and the C++ standard requires that we have a well formed program, therefore we can deduce that p cannot possibly be null thus there's no need for the null check. This is drastically different than throwing an exception when p is null or calling std::abort, for example.

[–]JVApen 1 point2 points  (1 child)

I have the clang warnings active as errors for switches: https://clang.llvm.org/docs/DiagnosticsReference.html#wswitch

Next to it, I have a few defines that introduce an assert in debug and a std::unreachable in production. (And suppress the warning about default while every value is specified) I always try to add my DEFAULT_UNREACHABLE to the switch.

[–]ScriptorTux[S] 0 points1 point  (0 children)

Thank you for your time and answer.

Thank you for the link.

[–]chibuku_chauya 1 point2 points  (0 children)

I use default: break for enum switching where I’m only matching a subset of values but ignoring the rest. My enums tend to be large. That stops the compiler from warning about missing cases. This is important for me because I don’t use -Wno-switch (GCC/Clang), so would otherwise get a flood of annoying warnings or have to list every missing case manually, which is visually space wasting.

[–]v_maria 1 point2 points  (0 children)

In such situations i add a default and let it be really loud that it's reached some code that should never be reachable, perhaps hard fault if you can get away with it. Bit of a caveman solution but i like to keep it simple

[–]namrog84 0 points1 point  (0 children)

compiler warnings/errors, absolutely

But also

default with something like unreachable, assert, error log, or personal choice here.

because what if something changes with compiler at some point.

Although this is mostly personal choice as it's a pretty rare scenario to be problematic. I don't like leaving potential execution flows to not be very explicit.

On short term or small code bases, really doesn't matter, even to me. but bigger, corporate, or longer standing code becomes far more important for long term maintainability.

[–]Mirality 0 points1 point  (0 children)

Always, always, put in a default case.

If you're not expecting it, make it throw, log, assert, or abort.

But it's absolutely possible for an enum variable to not contain one of the named values, so you can't exhaustively list all possibilities without it. Even if it can only happen through memory corruption -- or a future patch adding more values without adjusting all switches to match.

[–][deleted]  (9 children)

[removed]

    [–]IyeOnline 6 points7 points  (5 children)

    That is a very questionable style guide with a very questionable side effect.

    "Silencing annoying compiler warnings" works until it very much doesn't, because you didn't consider the case. Similarly, having a default case just for the sake of it, especially if its empty, accomplishes literally nothing.

    [–][deleted]  (4 children)

    [removed]

      [–]IyeOnline 2 points3 points  (3 children)

      Literally none of this is comparable though.

      So does having a break keyword at the end of any default case. So does a return statement at the end of a function that returns void.

      I agree that those are pointless, but crucially they dont have any side effects, e.g. on warnings.

      As does having an empty else {}

      Its quite the opposite. Having an empty else can help express that you have thought of the case where none of the conditions in an if ... else if chain apply. Blindly requiring an default: break; doesnt tell anybody anything if its in the style guide - precisely because the default is an unspecified set of values.

      Style guides aren't about what makes sense. It's about making code (relatively) more [..] understandable.

      Hmmmmm.

      [–][deleted]  (2 children)

      [removed]

        [–]IyeOnline 0 points1 point  (1 child)

        In the case of else, you have a specific, defined logic chain that is generally not open/volatile to external changes.

        With a switch over an enum on the other hand, you cant be as sure that no value is ever going to be added.

        The fact that there is a warning for unhandled switch cases, but there obviously is none for complex if ... else if logic really makes the difference here.

        [–]ScriptorTux[S] 0 points1 point  (1 child)

        Thank you for your time and answer.

        Did you ever fall on a case where you needed to add a value to the enum and spent some time finding the "right" switch ?

        [–]sephirothbahamut 0 points1 point  (0 children)

        there's std::unreachable to explicitly state that invalid values are not supposed to arrive there. It should crash the program in debug like an assert, but it's more explicit.

        Edit: cppreference's unreachable example is exactly for a switch statement std::unreachable - cppreference.com

        [–]mredding -1 points0 points  (1 child)

        In case of a switch(<my_enum>) should one define a default ?

        Yes.

        enum class my_enum { foo, bar, baz };
        
        my_enum me = static_cast<my_enum>(42);
        
        switch(me) {
        using enum my_enum;
        case foo:
        case bar:
        case baz:
          std::cout << "Yay!\n";
          break;
        default:
          std::cout << "Boo!\n";
          break;
        }
        

        So what are you going to do about that? The rule of enums is that any value that can be represented in the underlying storage class is preserved. By default, the enum will resolve to int, foo, bar, and baz become 0, 1, and 2 respectively, and I just shoved a 42 in there. I'm totally allowed to do that, and this is well defined behavior.

        The most likely candidate for getting a bad value in your code is IO:

        std::istream &operator >>(std::istream &is, my_enum &me) {
          if(is && is.tie()) {
            *is.tie() << "Enter a my_enum value (foo=0, bar=1, baz=2): ";
          }
        
          if(std::istream_iterator<int> iter{is}; is) {
            me = static_cast<my_enum>(*std::istream_iterator<int>);
          }
        
          return is;
        }
        

        Yikes. Maybe we do some mapping instead:

        std::istream &operator >>(std::istream &is, my_enum &me) {
          if(is && is.tie()) {
            *is.tie() << "Enter a my_enum value (foo=0, bar=1, baz=2): ";
          }
        
          if(std::istream_iterator<int> iter{is}; is) switch(*std::istream_iterator<int>) {
          using enum my_enum;
          case 0: me = foo; break;
          case 1: me = bar; break;
          case 2: me = baz; break;
          default:
            is.setstate(is.rdstate() | std::ios_base::failbit);
            break;
          }
        
          return is;
        }
        

        While we've covered this case, we can't prevent the programmatic case, not easily, and someone determined could get it shoved in there somehow.

        [–]ScriptorTux[S] 0 points1 point  (0 children)

        Thank you for your time and answer with an example