AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

03 Control flow / Lesson 2 of 5

switch — labels, fallthrough and what to switch on

13 min · +10 XP · 2 exercises · 7 questions

Read the text13 min
Exercises0/2
Quiz7 q

Quick answer

How does a switch statement work in C++?

A C++ switch compares one integral or enumeration value against constant case labels and jumps straight to the matching one, or to default if none matches. Execution then continues until a break; without one it falls through into the next label's statements. The labels must be compile-time constants, so a double or a std::string cannot be switched on.

A switch compares one integral value against a set of constants and jumps straight to the matching label. That is a different thing from an if chain, not a shorthand for one: the compiler may turn it into a jump table or a binary search, the labels must be compile-time constants, and control falls through from one label to the next unless you stop it. Every one of those properties is a feature when the switch is written for it and a bug when it is not. This lesson covers the anatomy, fallthrough, variables declared inside cases, and what to do instead of switching on a string.

Anatomy

#include <iostream>

int main() {
    char op;
    int a, b;
    std::cin >> a >> op >> b;
    switch (op) {
        case '+': std::cout << a + b << '\n'; break;
        case '-': std::cout << a - b << '\n'; break;
        case '*': std::cout << a * b << '\n'; break;
        case '/':
            if (b == 0) {
                std::cout << "division by zero\n";
            } else {
                std::cout << a / b << '\n';
            }
            break;
        default:
            std::cout << "unknown operator " << op << '\n';
            break;
    }
    return 0;
}

The controlling expression must have an integral or enumeration type — int, char, long long, bool (pointless), an enum or enum class, or a class type convertible to one of those. double is rejected, and so is std::string. Each case label is a constant expression of that type: a literal, a constexpr variable, an enumerator, 'a' + 1. Two labels with the same value are a compile error. default catches everything else and may appear anywhere, though convention puts it last.

break leaves the switch. Without it, execution continues into the statements under the next label — the switch is a list of labelled statements with one entry point per label, not a set of separate blocks.

Fallthrough, intended and accidental

Empty labels stacked on one body are the legitimate use of fallthrough and need no annotation:

switch (c) {
    case 'a': case 'e': case 'i': case 'o': case 'u':
        ++vowels;
        break;
    case ' ': case '\t':
        ++blanks;
        break;
    default:
        ++others;
        break;
}

A body that runs and then continues into the next body is where the bugs live — a forgotten break after case 1: silently adds case 2's work to case 1. GCC and Clang warn with -Wimplicit-fallthrough (part of GCC's -Wextra; Clang enables it only by name). When the fallthrough is what you want, say so with the C++17 attribute:

switch (level) {
    case 3:
        std::cout << "delete ";
        [[fallthrough]];
    case 2:
        std::cout << "write ";
        [[fallthrough]];
    case 1:
        std::cout << "read\n";
        break;
    default:
        std::cout << "no access\n";
        break;
}

[[fallthrough]]; is a statement with no run-time effect: it documents the intent and tells the compiler not to warn, which a comment cannot do. Level 3 prints all three words — the ladder accumulates.

Enums and the missing default

Switching on a scoped enum is the most common use in modern code, and it comes with a compiler check worth keeping:

enum class Shape { Circle, Square, Triangle };

int sides(Shape s) {
    switch (s) {
        case Shape::Circle: return 0;
        case Shape::Square: return 4;
        case Shape::Triangle: return 3;
    }
    return -1;   // reached only for a value that is none of the enumerators
}

With no default, -Wswitch (in -Wall) warns when an enumerator is not handled — so adding Shape::Hexagon later produces a warning at every switch that forgot it. Write a default and the check is gone. The price is the line after the switch: an enum class can hold a value that is none of its enumerators (static_cast<Shape>(7)), the compiler knows it, and a function without the trailing return "may reach the end of a non-void function". Handle every enumerator, omit default, keep the fallback after the switch.

Declaring a variable inside a case

switch (n) {                      // does not compile
    case 1:
        int doubled = n * 2;      // error: jump to case label crosses initialization
        std::cout << doubled;
        break;
    case 2:
        std::cout << n;
        break;
}

Every label is inside one scope — the switch body — so doubled is in scope at case 2 as well, but jumping to case 2 skips its initialisation. C++ forbids jumping past the initialisation of a variable, and the compiler says so. The fix is a block per case:

switch (n) {
    case 1: {
        int doubled = n * 2;
        std::cout << doubled;
        break;
    }
    case 2:
        std::cout << n;
        break;
}

Now doubled belongs to the block and no label can jump into it. The rule covers anything with an initialiser, including std::string s; (a constructor counts); a bare int x; would be allowed, and reading it would be undefined behaviour — brace the case instead.

No strings in a switch

switch (command) with std::string command does not compile: the labels must be constant expressions, and comparing strings is a run-time operation on text that lives on the heap. Three honest options:

  1. An if chain — if (cmd == "add") … else if (cmd == "sub") …. Fine for a handful of words.
  2. Map the word to an enum, then switch — an if chain (or a std::map<std::string, Cmd>) does the string work once, and the switch on the enum gets the exhaustiveness check and the jump table:
enum class Cmd { Add, Sub, Show, Unknown };

Cmd cmd = Cmd::Unknown;
if (word == "add") cmd = Cmd::Add;
else if (word == "sub") cmd = Cmd::Sub;
else if (word == "show") cmd = Cmd::Show;

switch (cmd) {
    case Cmd::Add: /* … */ break;
    case Cmd::Sub: /* … */ break;
    case Cmd::Show: /* … */ break;
    case Cmd::Unknown: std::cout << "unknown: " << word << '\n'; break;
}
  1. Switch on a character when the vocabulary is single letters (case 'q':) — the shape of most menu loops.

You will meet code that switches on a constexpr hash of the string: it compiles, it is fast, and two words can hash to the same label without the compiler noticing. Leave it where you found it.

switch with an initialiser (C++17)

Like if, switch can declare a variable scoped to itself:

switch (const int code = status(); code) {
    case 0: std::cout << "ok\n"; break;
    case 1: std::cout << "retry\n"; break;
    default: std::cout << "failed with " << code << '\n'; break;
}

code is visible in every case and gone afterwards. It is most useful when the default branch wants to print the value that matched nothing.

switch or if?

Use switch whenUse if when
one integral or enum value is compared against constantsthe tests are ranges (score >= 90) or compound (a && b)
several values share one body (stacked labels)the value is a double, a std::string or a pointer
you want -Wswitch to catch a missing enumeratorthe branches test different variables

What goes wrong

  • A missing break runs the next case's body too. Compile with -Wextra; write [[fallthrough]] when you mean it.
  • break inside a switch inside a loop leaves the switch, not the loop. Use a flag or restructure — the command-interpreter exercise makes you.
  • Duplicate case values and non-constant labels (case n:) are compile errors.
  • case 1 ... 5: is a GCC extension; it will not compile on other compilers. Use an if for ranges.
  • A default on an enum switch hides the missing-enumerator warning.

Key takeaways

  • switch dispatches on one integral or enum value to constant labels; break ends a case, and without it control falls through.
  • Stack empty labels freely; mark a body-to-body fallthrough with [[fallthrough]];.
  • A variable initialised inside a case needs braces around the case.
  • Strings cannot be switched on: map them to an enum first, or use an if chain.
  • Handle every enumerator and omit default so the compiler warns when the enum grows.

Common questions

What is fallthrough in a C++ switch?

Fallthrough is execution continuing from one case into the next because no break ended it. Stacked empty labels such as case 'a': case 'e': rely on it legitimately; a body running into the next body is usually a forgotten break, which -Wimplicit-fallthrough in -Wextra warns about. Mark an intended one with [[fallthrough]];.

Can you use a string in a switch statement in C++?

No. switch needs an integral or enumeration value and constant labels, and comparing strings is a run-time operation. Use an if chain for a few words, or map the word to an enum class once and switch on the enum, which also gets the compiler's missing-case warning.

What does "jump to case label crosses initialization" mean?

All case labels share one scope, the switch body, so a variable initialised under one label would still be in scope, uninitialised, at the next. C++ forbids jumping past an initialisation, so the compiler refuses. Wrap that case's statements in their own braces: case 1: { int d = n * 2; break; }.

Should a switch on an enum have a default case?

Usually not. With no default, -Wswitch (part of -Wall) warns whenever an enumerator is not handled, so adding a new value later flags every switch that forgot it. Handle every enumerator and put a fallback return after the switch, because an enum can still hold a value that matches none.

When should I use switch instead of if-else in C++?

Use switch when one integral or enum value is compared against constants, when several values share one body, or when you want -Wswitch to catch a missing enumerator. Use if for ranges such as score >= 90, compound conditions, or values that are a double, a string or a pointer.

Mark as readSign in to keep your place.

C++ Switch Statement: Case, Break and Fallthrough

A C++ switch jumps to the case matching one integral or enum value and falls through without break. The fallthrough attribute, enums, and switching on strings.

  • Course: C++ study plan
  • Module: Control flow
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: Clang 18 · C++20

How does a switch statement work in C++?

A C++ switch compares one integral or enumeration value against constant case labels and jumps straight to the matching one, or to default if none matches. Execution then continues until a break; without one it falls through into the next label's statements. The labels must be compile-time constants, so a double or a std::string cannot be switched on.

Lesson

A switch compares one integral value against a set of constants and jumps straight to the matching label. That is a different thing from an if chain, not a shorthand for one: the compiler may turn it into a jump table or a binary search, the labels must be compile-time constants, and control falls through from one label to the next unless you stop it. Every one of those properties is a feature when the switch is written for it and a bug when it is not. This lesson covers the anatomy, fallthrough, variables declared inside cases, and what to do instead of switching on a string.

Anatomy

#include <iostream>

int main() {
    char op;
    int a, b;
    std::cin >> a >> op >> b;
    switch (op) {
        case '+': std::cout << a + b << '\n'; break;
        case '-': std::cout << a - b << '\n'; break;
        case '*': std::cout << a * b << '\n'; break;
        case '/':
            if (b == 0) {
                std::cout << "division by zero\n";
            } else {
                std::cout << a / b << '\n';
            }
            break;
        default:
            std::cout << "unknown operator " << op << '\n';
            break;
    }
    return 0;
}

The controlling expression must have an integral or enumeration type — int, char, long long, bool (pointless), an enum or enum class, or a class type convertible to one of those. double is rejected, and so is std::string. Each case label is a constant expression of that type: a literal, a constexpr variable, an enumerator, 'a' + 1. Two labels with the same value are a compile error. default catches everything else and may appear anywhere, though convention puts it last.

break leaves the switch. Without it, execution continues into the statements under the next label — the switch is a list of labelled statements with one entry point per label, not a set of separate blocks.

Fallthrough, intended and accidental

Empty labels stacked on one body are the legitimate use of fallthrough and need no annotation:

switch (c) {
    case 'a': case 'e': case 'i': case 'o': case 'u':
        ++vowels;
        break;
    case ' ': case '\t':
        ++blanks;
        break;
    default:
        ++others;
        break;
}

A body that runs and then continues into the next body is where the bugs live — a forgotten break after case 1: silently adds case 2's work to case 1. GCC and Clang warn with -Wimplicit-fallthrough (part of GCC's -Wextra; Clang enables it only by name). When the fallthrough is what you want, say so with the C++17 attribute:

switch (level) {
    case 3:
        std::cout << "delete ";
        [[fallthrough]];
    case 2:
        std::cout << "write ";
        [[fallthrough]];
    case 1:
        std::cout << "read\n";
        break;
    default:
        std::cout << "no access\n";
        break;
}

[[fallthrough]]; is a statement with no run-time effect: it documents the intent and tells the compiler not to warn, which a comment cannot do. Level 3 prints all three words — the ladder accumulates.

Enums and the missing default

Switching on a scoped enum is the most common use in modern code, and it comes with a compiler check worth keeping:

enum class Shape { Circle, Square, Triangle };

int sides(Shape s) {
    switch (s) {
        case Shape::Circle: return 0;
        case Shape::Square: return 4;
        case Shape::Triangle: return 3;
    }
    return -1;   // reached only for a value that is none of the enumerators
}

With no default, -Wswitch (in -Wall) warns when an enumerator is not handled — so adding Shape::Hexagon later produces a warning at every switch that forgot it. Write a default and the check is gone. The price is the line after the switch: an enum class can hold a value that is none of its enumerators (static_cast<Shape>(7)), the compiler knows it, and a function without the trailing return "may reach the end of a non-void function". Handle every enumerator, omit default, keep the fallback after the switch.

Declaring a variable inside a case

switch (n) {                      // does not compile
    case 1:
        int doubled = n * 2;      // error: jump to case label crosses initialization
        std::cout << doubled;
        break;
    case 2:
        std::cout << n;
        break;
}

Every label is inside one scope — the switch body — so doubled is in scope at case 2 as well, but jumping to case 2 skips its initialisation. C++ forbids jumping past the initialisation of a variable, and the compiler says so. The fix is a block per case:

switch (n) {
    case 1: {
        int doubled = n * 2;
        std::cout << doubled;
        break;
    }
    case 2:
        std::cout << n;
        break;
}

Now doubled belongs to the block and no label can jump into it. The rule covers anything with an initialiser, including std::string s; (a constructor counts); a bare int x; would be allowed, and reading it would be undefined behaviour — brace the case instead.

No strings in a switch

switch (command) with std::string command does not compile: the labels must be constant expressions, and comparing strings is a run-time operation on text that lives on the heap. Three honest options:

  1. An if chain — if (cmd == "add") … else if (cmd == "sub") …. Fine for a handful of words.
  2. Map the word to an enum, then switch — an if chain (or a std::map<std::string, Cmd>) does the string work once, and the switch on the enum gets the exhaustiveness check and the jump table:
enum class Cmd { Add, Sub, Show, Unknown };

Cmd cmd = Cmd::Unknown;
if (word == "add") cmd = Cmd::Add;
else if (word == "sub") cmd = Cmd::Sub;
else if (word == "show") cmd = Cmd::Show;

switch (cmd) {
    case Cmd::Add: /* … */ break;
    case Cmd::Sub: /* … */ break;
    case Cmd::Show: /* … */ break;
    case Cmd::Unknown: std::cout << "unknown: " << word << '\n'; break;
}
  1. Switch on a character when the vocabulary is single letters (case 'q':) — the shape of most menu loops.

You will meet code that switches on a constexpr hash of the string: it compiles, it is fast, and two words can hash to the same label without the compiler noticing. Leave it where you found it.

switch with an initialiser (C++17)

Like if, switch can declare a variable scoped to itself:

switch (const int code = status(); code) {
    case 0: std::cout << "ok\n"; break;
    case 1: std::cout << "retry\n"; break;
    default: std::cout << "failed with " << code << '\n'; break;
}

code is visible in every case and gone afterwards. It is most useful when the default branch wants to print the value that matched nothing.

switch or if?

Use switch whenUse if when
one integral or enum value is compared against constantsthe tests are ranges (score >= 90) or compound (a && b)
several values share one body (stacked labels)the value is a double, a std::string or a pointer
you want -Wswitch to catch a missing enumeratorthe branches test different variables

What goes wrong

  • A missing break runs the next case's body too. Compile with -Wextra; write [[fallthrough]] when you mean it.
  • break inside a switch inside a loop leaves the switch, not the loop. Use a flag or restructure — the command-interpreter exercise makes you.
  • Duplicate case values and non-constant labels (case n:) are compile errors.
  • case 1 ... 5: is a GCC extension; it will not compile on other compilers. Use an if for ranges.
  • A default on an enum switch hides the missing-enumerator warning.

Key takeaways

  • switch dispatches on one integral or enum value to constant labels; break ends a case, and without it control falls through.
  • Stack empty labels freely; mark a body-to-body fallthrough with [[fallthrough]];.
  • A variable initialised inside a case needs braces around the case.
  • Strings cannot be switched on: map them to an enum first, or use an if chain.
  • Handle every enumerator and omit default so the compiler warns when the enum grows.

Common questions

What is fallthrough in a C++ switch?

Fallthrough is execution continuing from one case into the next because no break ended it. Stacked empty labels such as case 'a': case 'e': rely on it legitimately; a body running into the next body is usually a forgotten break, which -Wimplicit-fallthrough in -Wextra warns about. Mark an intended one with [[fallthrough]];.

Can you use a string in a switch statement in C++?

No. switch needs an integral or enumeration value and constant labels, and comparing strings is a run-time operation. Use an if chain for a few words, or map the word to an enum class once and switch on the enum, which also gets the compiler's missing-case warning.

What does "jump to case label crosses initialization" mean?

All case labels share one scope, the switch body, so a variable initialised under one label would still be in scope, uninitialised, at the next. C++ forbids jumping past an initialisation, so the compiler refuses. Wrap that case's statements in their own braces: case 1: { int d = n * 2; break; }.

Should a switch on an enum have a default case?

Usually not. With no default, -Wswitch (part of -Wall) warns whenever an enumerator is not handled, so adding a new value later flags every switch that forgot it. Handle every enumerator and put a fallback return after the switch, because an enum can still hold a value that matches none.

When should I use switch instead of if-else in C++?

Use switch when one integral or enum value is compared against constants, when several values share one body, or when you want -Wswitch to catch a missing enumerator. Use if for ranges such as score >= 90, compound conditions, or values that are a double, a string or a pointer.

Exercises

A command interpreter

Write an interpreter for a one-register machine. The register starts at 0 and holds a long long. Read commands one per line until quit or the end of input:

  • add N, sub N, mul N — apply the operation with the integer N
  • neg — negate the register
  • show — print value=<register>
  • reset — set the register to 0
  • quit — stop; nothing after it is processed
  • anything else — print unknown command: <word>

Map the word to an enum class first, then switch on the enum. Remember that break inside the switch leaves the switch, not the reading loop — quit needs a flag the loop condition checks.

Input: commands, one per line. Output: one line per show and per unknown command.

add 5
mul 3
show
sub 20
neg
show
quit

prints

value=15
value=5

A privilege ladder

Read an integer n, then n lines each holding a name and a one-letter role. Roles are cumulative: an admin (a or A) may delete, write and read; an editor (e or E) may write and read; a viewer (v or V) may read. Print <name>: <rights> with the rights in that order, separated by single spaces — delete write read, write read or read — or <name>: no access for any other letter.

Write one switch on the role character: stack the two letters of each role as empty labels, and let the cases fall into each other with [[fallthrough]]; so each level adds its word before the level below adds the next.

Input: n, then n lines of name role. Output: n lines.

3
ada A
bob e
cy v

prints

ada: delete write read
bob: write read
cy: read

In this module: Control flow

← Branching — if, else and the conditional operator · Loops — for, while, do…while and the traps between them →