Switch Fallthrough and Scoping

Fallthrough

When a switch matches a case label, execution begins at the first statement after that label and continues sequentially until one of these happens:

  • The end of the switch block is reached
  • A control flow statement (break or return) exits the switch
  • The program terminates

Importantly, encountering another case label does NOT stop execution - without break or return, execution "falls through" into the next case:

#include <iostream>

int main()
{
    switch (3)
    {
    case 1: // doesn't match
        std::cout << 1 << '\n'; // skipped
    case 2: // doesn't match
        std::cout << 2 << '\n'; // skipped
    case 3: // matches!
        std::cout << 3 << '\n'; // execution begins here
    case 4:
        std::cout << 4 << '\n'; // also executes (fallthrough)
    case 5:
        std::cout << 5 << '\n'; // also executes (fallthrough)
    default:
        std::cout << 6 << '\n'; // also executes (fallthrough)
    }

    return 0;
}

Output:

3
4
5
6

This is called fallthrough, and it's usually unintentional.

Warning
Without break or return, execution falls through to subsequent cases.
## The [[fallthrough]] attribute

Sometimes fallthrough is intentional. To indicate this and suppress compiler warnings, use the [[fallthrough]] attribute:

#include <iostream>

int main()
{
    switch (3)
    {
    case 1:
        std::cout << 1 << '\n';
        break;
    case 2:
        std::cout << 2 << '\n';
        break;
    case 3:
        std::cout << 3 << '\n'; // execution begins here
        [[fallthrough]]; // intentional fallthrough - semicolon required
    case 4:
        std::cout << 4 << '\n'; // also executes
        break;
    case 5:
        std::cout << 5 << '\n';
        break;
    }

    return 0;
}

Output:

3
4

The [[fallthrough]] attribute modifies a null statement (the semicolon) to tell the compiler that fallthrough is intentional.

Best Practice
Use `[[fallthrough]]` (with a null statement) to indicate intentional fallthrough.
## Sequential case labels

You can stack case labels to make multiple values execute the same code:

bool isWeekend(char day)
{
    switch (day)
    {
    case 'S': // if day is 'S'
    case 's': // or if day is 's'
        return true;
    case 'M': // if day is 'M'
    case 'm': // or if day is 'm'
    case 'T':
    case 't':
    case 'W':
    case 'w':
    case 'R':
    case 'r':
    case 'F':
    case 'f':
        return false;
    default:
        return false;
    }
}

The first statement after all the stacked case labels is return true, so any matching label executes that statement. This is not considered fallthrough and doesn't need [[fallthrough]].

Labels don't create scope

Unlike if statements (which create implicit blocks), case labels don't create new scopes:

if (condition)
    std::cout << "true\n"; // implicitly in a block

switch (1)
{
case 1: // does NOT create a block
    processValue(); // part of switch scope
    break; // part of switch scope
default:
    std::cout << "default\n";
    break;
}

All statements after case labels are part of the switch block's scope.

Variable declaration in switch statements

You can declare variables anywhere in a switch, but initialization has restrictions:

switch (2)
{
    int value; // OK: declaration before case labels
    int initialized{ 10 }; // ERROR: initialization before case labels

case 1:
    int result; // OK but bad practice: declaration in a case
    result = 100; // OK: assignment is allowed
    break;

case 2:
    int computed{ 50 }; // ERROR: initialization with subsequent cases
    result = 200; // OK: result declared above
    break;

case 3:
    break;
}

Variables declared in one case can be used in other cases because all statements share the same scope. However, initialization is disallowed if subsequent cases exist, since the switch could jump over the initialization, leaving the variable uninitialized.

The solution is to create an explicit block:

switch (2)
{
case 1:
{
    int temperature{ 72 }; // OK: initialization inside a block
    std::cout << temperature;
    break;
}

case 2:
{
    int humidity{ 60 }; // OK: separate block
    std::cout << humidity;
    break;
}

default:
    std::cout << "default\n";
    break;
}
Best Practice
If you need to define variables in a case, wrap the case body in a block.

Summary

Fallthrough occurs when execution continues from one case into subsequent cases without a break or return statement. This is usually unintentional and can cause bugs, which is why most compilers warn about it.

The [[fallthrough]] attribute explicitly marks intentional fallthrough, suppressing compiler warnings. It's placed before a null statement (semicolon) at the end of a case to indicate the fallthrough is deliberate.

Sequential case labels allow multiple values to execute the same code by stacking case labels without intervening statements. This is not considered fallthrough and doesn't require the [[fallthrough]] attribute.

Case label scope differs from if statements—case labels don't create new scopes. All statements after case labels are part of the switch block's scope, meaning variables declared in one case are visible in subsequent cases.

Variable initialization restrictions prevent you from initializing variables when subsequent cases exist, since execution could jump over the initialization and leave the variable uninitialized. The solution is wrapping case bodies in explicit blocks, creating proper scopes for initialization.

Understanding fallthrough and scoping rules helps you avoid common switch statement pitfalls and write more maintainable code. Use [[fallthrough]] when you genuinely want execution to continue, and create explicit blocks when you need variables with initialization.