Switch Statement Basics

Chaining multiple if-else statements together to test the same variable against different values is verbose and inefficient:

#include <iostream>

void printColorName(int colorCode)
{
    if (colorCode == 0)
        std::cout << "Red";
    else if (colorCode == 1)
        std::cout << "Green";
    else if (colorCode == 2)
        std::cout << "Blue";
    else
        std::cout << "Unknown color";
}

int main()
{
    printColorName(1);
    std::cout << '\n';

    return 0;
}

The variable colorCode gets evaluated multiple times (up to three times), and we have to verify each time that we're comparing the same variable.

C++ provides a better solution: the switch statement, which is optimized for comparing a single value against multiple possibilities:

#include <iostream>

void printColorName(int colorCode)
{
    switch (colorCode)
    {
    case 0:
        std::cout << "Red";
        return;
    case 1:
        std::cout << "Green";
        return;
    case 2:
        std::cout << "Blue";
        return;
    default:
        std::cout << "Unknown color";
        return;
    }
}

int main()
{
    printColorName(1);
    std::cout << '\n';

    return 0;
}

A switch statement evaluates an expression once, then executes code based on which value matches:

  • If the expression matches a case label, execution begins after that case label
  • If no case matches but a default label exists, execution begins after the default label
  • If no case matches and there's no default label, the entire switch is skipped

Starting a switch

Begin with the switch keyword, followed by an expression in parentheses:

switch (expression)
{
    // case labels go here
}

The expression must evaluate to an integral type (int, char, short, long, etc.) or an enumerated type, or be convertible to one. Floating-point types, strings, and most other non-integral types cannot be used.

For Advanced Readers
Why only integral types? Switch statements are often implemented using jump tables for performance. Jump tables use integral values as array indices to jump directly to the correct code, which is much faster than sequential comparisons. This optimization only works with integral types.
## Case labels

A case label is declared with the case keyword followed by a constant expression:

#include <iostream>

void printColorName(int colorCode)
{
    switch (colorCode) // colorCode evaluates to 1
    {
    case 0:
        std::cout << "Red";
        return;
    case 1: // matches this case
        std::cout << "Green"; // execution starts here
        return; // then returns to caller
    case 2:
        std::cout << "Blue";
        return;
    default:
        std::cout << "Unknown color";
        return;
    }
}

int main()
{
    printColorName(1);
    std::cout << '\n';

    return 0;
}

Output:

Green

Each case label value must be unique:

switch (code)
{
case 42:
case 42:  // error: duplicate value
case '*': // error: '*' is 42 in ASCII
}

The default label

The default label (often called the default case) handles all values that don't match any case:

#include <iostream>

void printColorName(int colorCode)
{
    switch (colorCode) // colorCode evaluates to 7
    {
    case 0:
        std::cout << "Red";
        return;
    case 1:
        std::cout << "Green";
        return;
    case 2:
        std::cout << "Blue";
        return;
    default: // no matching case
        std::cout << "Unknown color"; // execution starts here
        return; // then returns to caller
    }
}

int main()
{
    printColorName(7);
    std::cout << '\n';

    return 0;
}

Output:

Unknown color

The default label is optional, and there can only be one per switch. By convention, place it last.

Best Practice
Place the default case last in the switch block.
## No matching case and no default

If no case matches and there's no default, the switch does nothing:

#include <iostream>

void printColorName(int colorCode)
{
    switch (colorCode) // colorCode evaluates to 9
    {
    case 0:
        std::cout << "Red";
        return;
    case 1:
        std::cout << "Green";
        return;
    case 2:
        std::cout << "Blue";
        return;
    // no matching case and no default
    }

    // execution continues here
    std::cout << "Processing complete";
}

int main()
{
    printColorName(9);
    std::cout << '\n';

    return 0;
}

Output:

Processing complete

Using break

The examples above use return statements to exit the function. If you want to exit the switch but continue executing code after it, use a break statement:

#include <iostream>

void printColorName(int colorCode)
{
    switch (colorCode) // colorCode evaluates to 2
    {
    case 0:
        std::cout << "Red";
        break;
    case 1:
        std::cout << "Green";
        break;
    case 2:
        std::cout << "Blue"; // execution starts here
        break; // jump to end of switch
    default:
        std::cout << "Unknown color";
        break;
    }

    // execution continues here
    std::cout << " is the color";
}

int main()
{
    printColorName(2);
    std::cout << '\n';

    return 0;
}

Output:

Blue is the color
Best Practice
End each case with either break or return, including the last case.
## Label indentation

Labels don't define nested scopes, so they conventionally aren't indented:

// Preferred style
void printColorName(int colorCode)
{
    switch (colorCode)
    {
    case 0: // not indented
        std::cout << "Red";
        return;
    case 1:
        std::cout << "Green";
        return;
    case 2:
        std::cout << "Blue";
        return;
    default:
        std::cout << "Unknown color";
        return;
    }
}

This makes labels stand out and correctly implies that statements are part of the switch scope, not a nested scope.

Best Practice
Don't indent case labels. This makes them easily identifiable.
## Switch with enums

Switch statements pair naturally with enumerated types. Since enums have a fixed set of known values, switch can handle each value explicitly:

#include <iostream>

enum class Color
{
    red,
    green,
    blue
};

void printColor(Color color)
{
    switch (color)
    {
    case Color::red:
        std::cout << "Red";
        break;
    case Color::green:
        std::cout << "Green";
        break;
    case Color::blue:
        std::cout << "Blue";
        break;
    // No default needed - all cases covered
    }
}

int main()
{
    printColor(Color::green);
    std::cout << '\n';

    return 0;
}

Output:

Green

When all enum values are handled, no default case is needed. Many compilers will warn if you forget to handle an enumerator, catching bugs at compile time.

Key Concept
Using switch with enums provides compile-time safety. If you add a new enumerator later, the compiler can warn you about switch statements that don't handle it. This is much safer than if-else chains, which won't warn about missing cases.
Best Practice
When switching on an enum, handle all enumerators explicitly rather than using a default case. This allows the compiler to warn you if new enumerators are added later.

Switch vs if-else

Use switch when:

  • Testing a single integral/enumerated expression for equality
  • Comparing against a small number of known values
  • You want clear, readable code that shows all possible values

Use if-else when:

  • Testing non-equality comparisons (e.g., temperature > 90)
  • Testing multiple conditions (e.g., age >= 18 && hasLicense)
  • Checking if a value is in a range (e.g., score >= 60 && score <= 100)
  • The expression is not an integral type (e.g., floating-point numbers)
  • The expression evaluates to bool
Best Practice
Prefer switch over if-else when testing a single integral/enumerated expression for equality against a small set of values.

Summary

Switch statements provide an efficient way to compare a single value against multiple possibilities, eliminating the need for repetitive if-else chains. The switch expression evaluates once, then execution jumps to the matching case label.

Case labels define the possible values to match against, each requiring a unique constant expression. When a match is found, execution begins at that case label and continues until a break, return, or the end of the switch.

The default label handles all values that don't match any case, serving as a catch-all for unexpected inputs. While optional, it's conventionally placed last in the switch block.

Break statements exit the switch and continue execution after it, preventing unintended fallthrough to subsequent cases. Each case should end with break or return.

Label formatting follows the convention of not indenting case labels, making them easily identifiable and correctly implying they're part of the switch scope rather than creating nested scopes.

Enum integration makes switch especially powerful. When switching on an enum and handling all enumerators explicitly (without a default), the compiler can warn if new enumerators are added, providing compile-time safety that if-else chains cannot match.

Switch statements excel at comparing integral or enumerated types against known values, providing clearer and more maintainable code than equivalent if-else chains. For non-equality comparisons, range checks, or boolean expressions, if-else statements remain the better choice.