Elegant way of avoiding default in switch cases (using enum class)
Asked Answered
C

5

1

I have a enum class, for example:

enum class State{
            S1,
            S2,
            S3,
            S4
        };

And whenever I make a switch/case statement that might use this class, I would like to avoid using a "default" at all costs, to force me to write a statement for all possible cases. The idea is that, if I add a new case in this enum, "S5", due to the lack of a default, the compiler will send me a warning at every switch because not all cases are being covered. By doing so I will not end up forgetting the places where this new state might have a specific behavior to be implemented.

The thing is, there are various switch/cases where there is only implementation for some of the enum cases:

switch(state)
{
 case S1: 
          doSomething1();
          break;
 case S2: 
          doSomething2();
          break;
 case S3: 
          break;
 case S4:
          break;
}

But I'm not very fond of these various "empty" cases for the states that have no behavior, followed by a break. This is exactly where a "default" would be more elegant but as I just explained, that's what I want to avoid.

Is there a more elegant (or efficient?) way of achieving what I'm trying to do here? Because I know other programming languages offer more advanced syntax for the switch/case statement, but I'm not sure about the possibilities with C++ (more specifically C++17).

Centrosphere answered 13/7, 2021 at 9:18 Comment(6)
break of S3 is not needed.Triennium
The case clauses "fall-through" so you could group all the unused cases together with a single break (and a comment explaining this to your future self).Egress
As an aside, for simple 1:1 relations between values and (unparametrized) actions like here you could use an array of functors/function pointers indexed by the enum value, or a map if the enums are not consecutive and 0-based. Admittedly I don't see how that would address your completeness question though.Teri
You can use if-statement for some special cases instead of switch.Patmore
Unfortunately the compiler is right, because you didn’t cover all cases; the default is unfortunately necessary, and should not be empty (instead it should raise an assertion failure). The reason is that a variable of your enum can contain other values than the ones you’ve declared.Melodious
Note that an enum can have values that are outside its stated values but that still fit into its underlying type and the only way (more or less) that you can catch those is with a default.Jarlen
R
2

For switch cases I prefer to do something like:

enum class my_enum {
    E1,
    E2,
    E3,
    E4,
    E5
};

result_type do_action_based_on_enum(my_enum e)
{
    switch(e) {
    case my_enum::E1:
        return action_e1();
    case my_enum::E2:
        return action_e2();
    case my_enum::E3:
    case my_enum::E4:
    case my_enum::E5:
        return action_default();
    }
}

Return in each case to avoid writing break each time, and fallthrough test cases when appropriate.

Rowboat answered 13/7, 2021 at 9:26 Comment(3)
Maybe you can throw exception with default case so user can remember that they did not add new case.Tetrafluoroethylene
throw outside the switch case, as enum might have value outside their provided value, and so you should have warning for "not all paths return for non void return type function".Triennium
@enumerator: not in default, as OP mentions, but outside is ok.Triennium
E
1

We may also use an associate container to store the handlers(lambdas or functors), then call the handlers after finding the key.

Add use assertion to ensure that all enums have the corresponding handlers (But it is not a compile-time error, we can store function pointer here to get a compile-time check but it makes the code ugly).

#include <cassert>
#include <functional>
#include <iostream>
#include <map>

enum class State { S1, S2, S3, S4, Count };

int main(int argc, char* argv[]) {
  auto dummy_handler = []() {};
  std::map<State, std::function<void()>> mapping = {
      {State::S1, []() { std::cout << "s1\n"; }},
      {State::S2, []() { std::cout << "s2\n"; }},
      {State::S3, dummy_handler },
      {State::S4, dummy_handler },
  };
  assert(mapping.size() == static_cast<size_t>(State::Count)); //May use this
  // line to ensume all handlers are set

  auto dispatch = [&](State e) {
    if (auto itr = mapping.find(e); itr != mapping.end()) {
      itr->second();
    }
  };

  auto e = State::S1; // Handlers for s1 will be called
  dispatch(e);

  e = State::S3;
  dispatch(e);  // handler for s3
  return 0;
}

Elwell answered 13/7, 2021 at 9:43 Comment(1)
I like containers but with a map it is not possible to have compile-time asserts for completeness (which the OP would like). Using an array instead of a map would permit that but is restricted to contiguous, 0-based enums. One should think that some clever template meta programming should be possible; or perhaps a user defined type with user defined literals?Teri
T
1

"Elegant" is in the eye of the beholder; but I have found that using macros to generate code sequences handling enums greatly reduces redundancy and the chance for errors. It is one of the few justified cases (beyond conditional compilation and includes) to use the preprocessor. As is often the case this pays more dividends in large and distributed projects.

The idea is to generate the enumeration itself as well as switch cases and containers such as maps or arrays from the same text, which must be in a separate file so that the same copy can be included several times. That makes it physically impossible to have enum members without an associated action. Adding another state is as trivial as adding a line to enum-actions.h; all uses like map entries, switch cases or else-if chains that are generated from that list are automatically adapted. These places still need to be recompiled because of the include dependency, but they don't need to be touched.

Here is the file which contains the enum member names, values and associated actions (which must be function names or functors at the time the switch/case is generated). I gave it a .h suffix because it will be included, even though it is not grammatical C++; one could also give it a .txt suffix.

enum-actions.h

// A list of enum identifiers with associated values 
// and actions usable in macro definitions.
// This macro is used to construct both the actual enum
// as well as the cases in the action switch. This way
// it is impossible to have enum members without an associated action.

// There is no "real" enum definition elsewhere; this is it.

ENUM_ACTION(S1, 2, action1)
ENUM_ACTION(S2, 23, action2)
ENUM_ACTION(S3, 997, no_action)

stateE.h

// Define the states enum from the macro list of enum/action pairs.
// The last member will have a trailing comma as well, which 
// is permitted by the C++ grammar exactly for this use case of code generation.
enum stateE
{
#   define ENUM_ACTION(state, value, action) state = value,
#   include "enum-actions.h"
#   undef ENUM_ACTION
};

associative-enum.cpp

#include <iostream>
#include "stateE.h"

// Dummy actions for states
void action1() { std::cout << __func__ << "\n"; }
void action2() { std::cout << __func__ << "\n"; }

// pseudo action when nothing should happen
void no_action() { std::cout << __func__ << "\n"; }


/// Perform the action associated with the state. This is done with a 
/// switch whose cases are constructed from the list
/// in enum-actions.h.
void actOnState(stateE stateArg)
{
    switch (stateArg)
    {
#       define ENUM_ACTION(state, value, action) case state: action(); break;
#       include "enum-actions.h"
#       undef ENUM_ACTION
    }
}

int main()
{
    actOnState(S1);
    actOnState(S2);
    actOnState(S3);
}

Sample session:

$ g++ -Wall -o associative-enum associative-enum.cpp && ./associative-enum
action1
action2
no_action

Teri answered 13/7, 2021 at 10:50 Comment(2)
Your idea seems similar to the one used in LLVM's code base, it's coolElwell
@Elwell Ah, I didn't know that! I don't even know from whom I learned it 20 years or so ago, or if I "invented" it myself. It is in any case a dead sure method to not overlook any values.Teri
P
0

If you use compiler g++ than you can add compiler option -Wall or -Wswitch. For example, g++ -std=c++17 -Wall main.cpp or g++ -std=c++17 -Wswitch main.cpp. For code

#include <iostream>

enum class State{
            S1,
            S2
        };

int main()
{
    State state = State::S1;
    switch (state) {
        case State::S1:
            std::cout << "State::S1" << std::endl;
            break;
    }
}

you can get compilation error:

main.cpp:11:12: warning: enumeration value 'S2' not handled in switch [-Wswitch]
Patmore answered 13/7, 2021 at 9:40 Comment(1)
If S2 doesn't need to be handled here the warning needs to be ignored but would camouflage the case a S3 is added which does need to be handled. The OP would like to compile without warnings and without default but also without explicit empty cases for unhandled values.Teri
C
0

Pretty old thread but I thought I could add a solution I found which wasn't suggested here. I was looking for a way to prevent compilation altogether if the switch statement didn't use all the enum values, so what I ended up doing is the following:

    enum class MyEnum : uint8
    {
        E1,
        E2,
        Count
    }

    void RandomFunc()
    {
        switch (MyEnum)
        {
            case MyEnum::E1: /* Do your things */ break;
            case MyEnum::E2: /* Do your things */ break;
            default: 
                static_assert(static_cast<uint8>(MyEnum::Count) == 2, "If you removed or added a MyEnum value, please update the code above");
        }
    }

The static_assert itself kind of needs to be in the default statement to avoid getting warnings (I don't like this part of my solution but it's the best I found so far) in most cases. This ensures no one can add or remove an enum value without updating the associated code. However, in your specific case, I think it'd be safe to put it outside the switch statement and use the default case for your default behaviour.

enum class MyEnum : uint8
{
    E1,
    E2,
    E3,
    E4,
    Count
}

void RandomFunc()
{
    static_assert(static_cast<uint8>(MyEnum::Count) == 4, "If you removed or added a MyEnum value, please update the switch statement below");
    switch (MyEnum)
    {
        case MyEnum::E1: // Do your things
        case MyEnum::E2: // Do your things
        default: // Your default behaviour for E3 and E4
    }
}

If you really don't want to use a default statement despite using a separate static_assert, you can use the fall-through method described by other answers.

A non-compilation time error generation could also be to add a

check(false) 

statement (or whatever error statement your project uses) in the default case. This makes it obvious to anyone reading your code that there isn't a default statement wanted and they need to update the cases. I wouldn't recommend doing both as static_assert already covers compile-time so covering runtime wouldn't make much sense. But it improves readability too so I could understand using both.

Crass answered 3/6 at 11:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.