Why use switch?

So, maybe this is a bigger discussion, but why use a switch statement at all if you aren’t in some cases passing through? i.e. you don’t actually break at the end of case A for example because you also want it to run the code for case B.

As I understand it those are the situations where using switch statements are “best”?

Why not use if statements?

Is it just a preference thing? From what I understand to the compiler it’s not that much different.

2 Likes

Actually, I didn’t really think too much about that. I’m a web backend developer, and… I try to never use switch statements, because I just don’t like it. I think it’s not the best readable code.

When I code on Arduboy or Arduino, It’s like an experimentation. So, maybe, unconsciously I used it because I won’t use it on my day to day job :smiley:

But if someone has a more objective answer, I’d like to know too !

1 Like

If statements need to be evaluated one by one, where switch will generate a jump table based on an integer value. Case statements only serve as labels and do not define a block, hence the fall-through behaviour.

3 Likes

That’s interesting. I find it much more readable than a bunch of nested ifs.

Exactly. There is a c++ attribute called [[fallthrough]] which lets you mark the end of the case explicitly to show that it is going to fall through to the next case.

switch (state) {

  case State::Init:

    // do init stuff
    state = State::A;
    [[fallthrough]];

  case State::A

    performState(); 
    break;

}

The [[fallthrough]] doesn’t really do much other than highlight your intent however it does surpress the compiler warning.

3 Likes

It’s a good thing to have, though, since contrary to @bateske’s assumption, making use of the fall through capabilities is rarely done, and not including a break has likely been done in error.

1 Like

Agreed it is rare to use fall through. However, I have been using fall through lately when creating an overall state machine for games.

enum class State : uint8_t {
  TitleScreen_Init,
  TitleScreen,
  PlayGame_Init,
  PlayGame,
};

State state = State::TitleScreen_Init;

void loop() {

  switch (state) {
    
    case State::TitleScreen_Init:
      // do one off stuff
      state = State::TitleScreen;
      [[fallthrough]]

    case State::TitleScreen:
      // render state and wait for keypress ..
      if (arduboy.pressed(A_BUTTON)) state = State::PlayGame_Init;
      break;

    case State:: PlayGame_Init:
      ....

}

There are probably better ways to do this, but its nice and simple!

Aside from if/else chains or switch/case, another (maybe not so common) technique for handling state branching is to use an array of function pointers with the state number used as the array index. I used this for the setSystemEEPROM example sketch in the Arduboy2 library.

There is a nice article on this in the Arduboy Magazine, Volume 4, Page 14 https://issuu.com/arduboymag/docs/arduboy_magazine_vol4_final_single

When I first read about this I thought it was neat. However, it actually used more memory in my example at the time so I dumped it. I should probably revisit it as it might be more efficient in different circumstances.

1 Like

No, it does tend to generate more code than other methods. Plus, it doesn’t help too much with readability if there are only a few states. However, if size isn’t a problem and you have many states, it can be a good choice.

1 Like

I didn’t say it was done all the time, I was saying it’s the only reason I can think to use a switch that provides any different performance than a series of if statements.

Actually, if statements have proven to frequently be more efficient than an equivalent switch in the Arduino AVR environment. The advantage of using switch is, for most people, it tends to be more readable and easier to write and maintain (as long as you don’t forget the break statements).

2 Likes

I have found that the order of the switch’s case satements can directly impact the code size. I have also gone back and removed switches (at the expense of readability) when having to squeeze those last bytes to complete a game.

5 Likes

Actually, I wouldn’t used nested if statements, I never do.

In that case by example, I would have done something like this :

if (screen == Screen::A) {
    // do stuff
    return;
}

if (screen == Screen::B) {
    // do stuff
    return;
}

 // do default stuff

Interesting !

1 Like

Yes, returning early makes the code much more readable than nested ifs. This too can generate smaller code than nesting the ifs together.

A lot of the time, I am surprised what influences the compiler. And just when I think I have worked out a trick for saving a few bytes the next time I implement it, it gives me the opposite result! Just keeps me guessing.

1 Like

Why not else if?

if (screen == Screen::A) {
    // do stuff
} 
else if (screen == Screen::B) {
    // do stuff
} 
else {
 // do default stuff
}

This is a personal preference I think. With early returns, you remove one identation at the end, which brings more readability.

I also think that a simple if is more readable that else if.

You can find some articles on that pattern : https://www.itamarweiss.com/personal/2018/02/28/return-early-pattern.html

Interesting. I’ve always found else-if to be more readable in this scenario because it more clearly indicates that the condition will only be checked if the previous one failed. Otherwise it reads like both could be executed depending on what happens inside the first if.

For example

if (someValue == 0)
{
    //do stuff
    someValue = 1;
}
if (someValue == 1)
{
    //do more stuff
}

the above scenario can be useful, but it can sometimes be unclear if it was intentional to have the second if block execute even if the first passed.

1 Like

One rather genius application of the fall-through is Duff’s Device. Note again that switch statements are pure pointer arithmetic and thus much faster than evaluating expressions in a (long) chain of if statements.

1 Like

I’m going to end up repeating what some of the others have said but my explanation is hopefully more thorough.

In regard to switches:

If by ‘passing through’ you’re referring to case fallthrough,
allowing cases to fall through is actually heavily frowned upon and a lot of languages don’t allow it because of the problems it can cause.

C# requires the break; and gives a compiler error if you don’t have it.
Functional languages use something called pattern matching that behaves similarly but has more uses than just a direct comparison.

Fallthrough is enough of a problem that most compilers warn you if you try to do it and C++17 introduced a new attribute (the [[fallthrough]] attribute @filmote mentioned) to make it more obvious when fallthrough is intentional.

Realistically break; ought to be the default behaviour and there ought to be an optional fallthrough; statement, but it’s too late to change it now.
(And once again, this is something C++ inherited from C, so I blame C. :P)

Part of the original intent of switch statements is that they could be optimised as jump tables (as @Prototype mentioned), which are much faster and more efficient than a chain of comparisons.
However, this isn’t always possible and isn’t always done.
(I’m under the impression that GCC compiling for AVR won’t actually generate jump tables, but only based on anecdotal evidence.)

Sometimes it will compile to a lookup table instead.
I.e. if the variable being tested is an integer or an enumeration and all the values in the case labels are sequential then the compiler may decide to create an array and use an array index instead.

Of course that’s something the programmer could do manually as well, and sometimes the programmer can do a better job of it, but sometimes it’s easier to let the compiler decide when to do that.

I’m almost certain that GCC compiling for AVR will generate these because I’ve manually replaced switches with lookup tables when trying to save memory and found that the memory usage didn’t change, implying that the compiler had already done so.

Even when no optimisations are possible, switch statements are typically much easier to read than a long chain of if statements.
With a chain of if statements it’s easy to be caught out when one of the tests suddenly isn’t an equality test.
With a switch the rules are easier to understand.

To be technical, it’s more of a declarative construct than a procedural construct.


I won’t say I never use nested ifs,
but I agree that breaking early using an inversed condition often results in more readable code,
and it’s something I try to do when I can.

One of the most accessible examples is:

if(!arduboy.nextFrame())
  return;

Which makes life a lot easier than wrapping the entire contents of loop in an if(arduboy.nextFrame()).

In particular, I often have an early continue in loops.
E.g. instead of:

for(size_t index = 0; index < size; ++index)
{
  auto & enemy = enemies[index];

  if(enemy.isAlive())
  {
    // Do enemy logic
  }
}

I’d prefer:

for(size_t index = 0; index < size; ++index)
{
  auto & enemy = enemies[index];

  // I.e. dead enemies are ignored
  // Any code following this can assume that only alive enemies are handled
  if(!enemy.isAlive())
    continue;

    // Do enemy logic
}

I find it better to specify what’s ignored rather than what’s included.
Process of elimination rather than process of inclusion.

That said, I disagree that switch statements are less readable.
I prefer them over if statements because they’re less surprising and generally better for expressing state machines.
(And can result in better code optimisation.)

Every case is always equivalent to an ==, but the same is not true of every if condition - you could encounter a long chain of == and then suddenly hit a != and if you’re not careful you could miss that.

I can’t think of many realistic scenarios where someone might attempt that.
If they did I’d probably frown at them.

Here’s a more realistic example of early exiting, taken from my short-lived attempt at writing a new String alternative following the wifi thread:

AltString::AltString(const __FlashStringHelper * progmemString)
{
	if(progmemString == nullptr)
	{
		this->invalidate();
		return;
	}

	const size_t size = (strlen_P(reinterpret_cast<const char *>(progmemString)) + 1);

	this->buffer = new char[size],
	this->capacity = size,
	this->length = (size - 1);

	memcpy_P(this->buffer, progmemString, size);
}

Where invalidate is:

void AltString::invalidate()
{
	delete[] this->buffer;

	this->buffer = nullptr;
	this->capacity = 0;
	this->length = 0;
}

Identify the erroneous/unique circumstances, then exit early.

Ideally it shouldn’t make a difference, but the order in which a compiler encounters code constructs can affect which optimisations it chooses.
Part of that is because of the possibility of fallthrough and crossed initialisation.

If fallthrough weren’t possible and each case were considered a separate scope then the compiler could (probably) truly pretend that the order didn’t matter and it would (probably) be more likely that order wouldn’t make a difference.

(If I were designing a programming language, I would probably do away with fallthrough and make each case a seperate block for precisely that reason.
At the very least I would make no fallthrough the default and require fallthrough to be marked explicitly.)

I wouldn’t call it genius, I’d call it evil.
(The fact the code is written in pre-ANSI C is a big hint that it comes from the dark ages. :P)

I’ll concede that much.
Yet more proof for the point that switch permits better compiler optimisation.
(Most of the time anyway.)

2 Likes

Is now a good time to bring up the case of a case like this:

switch (i) {
   case 0 ... 1:   return true;
   case 2 ... 6:   return false;
   default:        return theOtherState;
}

Now, don’t tell me they are non-standard and compiler specific. In my Arduboy world they work and I don’t care about portability. That’s the porter’s problem.

Of course, they are not an == and the default looks like an else.

1 Like