Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 2 of 6

When you're comparing one value against a list of constants, switch is clearer than a long if/else if chain:

#include <iostream>

int main() {
    int day = 3;

    switch (day) {
        case 1:
            std::cout << "Monday\n";
            break;
        case 2:
            std::cout << "Tuesday\n";
            break;
        case 3:
            std::cout << "Wednesday\n";
            break;
        default:
            std::cout << "Some other day\n";
            break;
    }
    return 0;
}
Wednesday

Execution jumps straight to the matching case, runs until it hits break, then leaves the switch. default catches everything unmatched.

The rules and the limits

  • The switch value must be an integer type or an enum — int, char, bool, std::size_t. Not std::string, and not double.
  • Each case label must be a compile-time constant (lesson 3.7). You can't switch on a range or a variable.
  • Case values must be unique.

If you need string matching or ranges, use if/else if.

Fallthrough: the missing break

This is the feature everyone gets bitten by once. Without break, execution falls through into the next case:

#include <iostream>

int main() {
    int level = 1;

    switch (level) {
        case 1:
            std::cout << "one\n";
        case 2:
            std::cout << "two\n";
        case 3:
            std::cout << "three\n";
            break;
        default:
            std::cout << "other\n";
    }
    return 0;
}
one
two
three

level is 1, so it matches case 1 — and then keeps going through cases 2 and 3 until it finally hits a break. The case labels are entry points, not self-contained blocks.

Nine times out of ten this is a bug: a forgotten break. Because it produces plausible-looking output rather than a crash, it can survive testing.

Deliberate fallthrough

The tenth time it's exactly what you want — grouping several cases that share behaviour:

#include <iostream>

int main() {
    char grade = 'B';

    switch (grade) {
        case 'A':
        case 'B':
        case 'C':
            std::cout << "Pass\n";
            break;
        case 'D':
        case 'F':
            std::cout << "Fail\n";
            break;
        default:
            std::cout << "Unknown grade\n";
            break;
    }
    return 0;
}
Pass

Stacked labels with no code between them are idiomatic and read fine — nobody misreads that as a missing break.

The ambiguous case is falling through after running code. C++17 added an attribute to say you meant it:

#include <iostream>

int main() {
    int verbosity = 2;

    switch (verbosity) {
        case 2:
            std::cout << "detailed info\n";
            [[fallthrough]];
        case 1:
            std::cout << "basic info\n";
            break;
        default:
            break;
    }
    return 0;
}
detailed info
basic info

[[fallthrough]]; documents the intent to the next reader and silences the compiler warning (-Wimplicit-fallthrough, included in -Wextra). Without it, a reviewer can't tell your deliberate fallthrough from a bug.

Declaring variables inside a case

A subtle rule: you can't declare a variable directly inside a case without braces, because later cases would jump over its initialization:

// WILL NOT COMPILE
int main() {
    int option = 1;
    switch (option) {
        case 1:
            int value = 42;   // jumped over by case 2
            return value;
        case 2:
            return 0;
    }
    return 0;
}

error: jump to case label ... crosses initialization of 'int value'

Add braces to give it a scope of its own:

#include <iostream>

int main() {
    int option = 1;

    switch (option) {
        case 1: {
            int value = 42;
            std::cout << value << "\n";
            break;
        }
        case 2:
            std::cout << "two\n";
            break;
    }
    return 0;
}
42

Always write default

Even when you think you've covered everything:

#include <iostream>

int main() {
    int status = 99;

    switch (status) {
        case 0:
            std::cout << "OK\n";
            break;
        case 1:
            std::cout << "Warning\n";
            break;
        default:
            std::cout << "Unexpected status: " << status << "\n";
            break;
    }
    return 0;
}
Unexpected status: 99

Without default, an unexpected value does nothing at all — silently. A default that reports the surprise turns a mystery into a message.

Try it

Write a calculator that takes a char operator ('+', '-', '*', '/') and two double values, and prints the result using a switch. Handle division by zero, and handle an unknown operator.

Checkpoint

  • switch compares one integer or enum value against compile-time constant case labels.
  • It can't switch on std::string, double, or ranges — use if/else if for those.
  • Without break, execution falls through into the next case; that's usually a bug.
  • Stacked empty labels are idiomatic; use [[fallthrough]]; when falling through after code.
  • Wrap a case in braces if you declare a variable inside it.
  • Always write a default, even if only to report the unexpected.

Questions about this lesson

Log in to ask a question.