JavaRush /Курсы /C++ SELF /switch в C++: ветвление по значению, break, fallthrough

switch в C++: ветвление по значению, break, fallthrough

C++ SELF
19 уровень , 2 лекция
Открыта

1. Введение

Вы уже давно знакомы с оператором if/else. Он идеально подходит, когда вы проверяете условия:

  • число больше/меньше (x > 10);
  • строка пустая (title.empty());
  • несколько условий вместе (a % 2 == 0 && b % 2 == 0).

Но иногда задача другая по природе: «Есть одно значение, и оно равно одному из вариантов — выбери нужную ветку».

Классический пример — команды меню:

if (cmd == 1) {
    /* add */
} else if (cmd == 2) {
    /* list */
} else if (cmd == 3) {
    /* done */
} else if (cmd == 0) {
    /* exit */
} else {
    /* unknown */
}

Работает? Да. Читается? Пока команд 3–4 — тоже да. Но когда команд становится 10, 15, 20, цепочка превращается в «паровозик условий».

Оператор switch говорит ту же мысль проще: «ветвимся по значению»:

switch (cmd) {
    case 1: /* add */  break;
    case 2: /* list */ break;
    case 3: /* done */ break;
    case 0: /* exit */ break;
    default: /* unknown */ break;
}

Если очень по‑человечески, то if/else if — это «переспрашивать у каждого охранника», а switch — это «сразу подойти к нужной двери с номером».

2. Как работает switch

switch — это оператор (statement), а не выражение

В C++ switch не «возвращает значение». Он просто выбирает, какой кусок кода выполнить. Он передаёт управление на подходящую метку (если значение совпадает).

Пример «переводчика» статуса в строку (мы уже делали похожее с enum class):

std::string ToString(Status s) {
    switch (s) {
        case Status::Todo:       return "todo";
        case Status::InProgress: return "in_progress";
        case Status::Done:       return "done";
    }
    return "unknown"; // страховка, чтобы функция точно возвращала строку
}

case — это метка, а не отдельный блок

Механика такая:

  1. вычисляется выражение в switch (expr) (один раз);
  2. управление прыгает на подходящую метку case ...: (или default:);
  3. дальше выполнение идёт вниз, пока вы не остановите его break/return.

Небольшая схема:

flowchart TD
    A["вычислили expr"] --> B{"switch(expr)"}
    B -->|нашли case| C["выполняем код case"]
    C --> D{"встретили break/return?"}
    D -- нет --> E["fallthrough: проваливаемся ниже"]
    D -- да --> F["выходим из switch"]

3.3. Fallthrough: «проваливание» в следующий case

Оператору switch уже много десятилетий, поэтому он унаследовал поведение, которое когда-то казалось удобным и логичным. По умолчанию выполняется не только выбранная ветка, но и все следующие за ней. Чтобы прекратить выполнение, нужно написать break.

Самая классическая ситуация:

switch (cmd) {
    case 1:
        std::cout << "Add\n";
    case 2:
        std::cout << "List\n";
}

Если cmd == 1, вы увидите и Add, и List. Потому что break забыли.

Исправление скучное, но спасает жизнь:

switch (cmd) {
    case 1:
        std::cout << "Add\n";
        break;	// заканчивает switch
    case 2:
        std::cout << "List\n";
        break;	// заканчивает switch
}

4. На что можно делать switch (а на что нельзя)

В C++ у оператора switch есть довольно жёсткие ограничения. В учебной практике можно считать так: передавать в него можно только 2 «семейства» типов — целые и перечисления.

Можно switch Примеры
Целые числа
int, char, long long, unsigned
Перечисления
enum, enum class
Нельзя напрямую switch Что делать вместо этого
double / float
обычно if/else (и осторожно с точностью)
std::string / std::string_view
сначала распарсить в enum class или код команды

Если команда приходит в виде строки

Например, вы хотите команды add, list, exit. switch по строке нельзя, поэтому делаем «переводчик»:

#include <string_view>

enum class Command { Add, List, Exit, Unknown };

Command ParseCommand(std::string_view s) {
    if (s == "add")  return Command::Add;
    if (s == "list") return Command::List;
    if (s == "exit") return Command::Exit;
    return Command::Unknown;
}

И дальше switch уже по Command:

switch (ParseCommand(cmdStr)) {
    case Command::Add:  /* ... */ break;
    case Command::List: /* ... */ break;
    case Command::Exit: /* ... */ break;
    case Command::Unknown:
        std::cout << "Unknown command\n";
        break;
}

5. case должен быть compile-time константой

Более того, значение после case должно быть известно компилятору заранее.

Хорошие варианты:

switch (cmd) {
    case 0: /* ... */ break;
    case 1: /* ... */ break;
}
constexpr int kExit = 0;

switch (cmd) {
    case kExit:
        return 0;
}

И идеальный вариант для «фиксированных вариантов» — enum class:

switch (status) {
    case Status::Todo:       /* ... */ break;
    case Status::InProgress: /* ... */ break;
    case Status::Done:       /* ... */ break;
}

Плохой (не скомпилируется) вариант — case по переменной, которую мы узнаём только во время выполнения:

int x = ReadFromUser();

switch (cmd) {
    case x: // ошибка: x не compile-time константа
        break;
}

6. break и return: чем заканчивать ветки

Чтобы избежать досадных ошибок, полезно держать в голове практическое правило: каждый case должен заканчиваться break или return. Если break нет — это должно быть осознанно и отмечено специальной конструкцией [[fallthrough]].

break выходит только из switch

Новички иногда ждут, что break «выйдет из всего». Но break завершает ближайший switch или цикл.

Если у вас switch внутри while, то break выйдет из switch, а цикл продолжится:

while (true) {
    switch (cmd) {
        case 0:
            break; // выйдет из switch, но while продолжится
    }
}

Если вы в меню и хотите реально закончить программу, обычно проще сделать так:

case 0:
    return 0; // выход из main -> программа завершилась

continue внутри switch в цикле — это continue цикла

Это не «перейти к следующему case». Это «перейти к следующей итерации цикла».

while (true) {
    switch (cmd) {
        case 1:
            // ...
            continue; // продолжает while
    }
}

Иногда это удобно, но если вы не ожидали — выглядит как магия (плохая магия).

7. Намеренный fallthrough и [[fallthrough]]

Иногда проваливание реально нужно: один case делает «добавку», а потом выполняет общий код следующего case.

Пример с уровнями логов (просто чтобы увидеть идею):

#include <iostream>

enum class Level { Info, Warning, Error };

void PrintPrefix(Level lvl) {
    switch (lvl) {
        case Level::Error:
            std::cout << "[ERR] ";
            [[fallthrough]]; // намеренно идём дальше
        case Level::Warning:
            std::cout << "[ATTN] ";
            break;
        case Level::Info:
            std::cout << "[INFO] ";
            break;
    }
}

Зачем [[fallthrough]]?

Он делает две полезные вещи:

  1. читателю кода видно, что break не забыли;
  2. компилятор (часто) перестаёт предупреждать «возможно забыли break».

7.1. «Склейка case» — это другое (и обычно без [[fallthrough]])

Если вы хотите, чтобы несколько значений вели в один блок, вы просто пишете несколько меток подряд, без кода между ними:

bool IsActive(Status s) {
    switch (s) {
        case Status::Todo:
        case Status::InProgress:
            return true;
        case Status::Done:
            return false;
    }
    return false;
}

Здесь проваливание «безопасное»: между метками нет действий, поэтому никто не забудет break «случайно».

8. default: когда он нужен, а когда прячет проблему

default — это реакция на «что угодно другое». Своеобразный else для switch.

Когда default очень уместен

Если значение приходит из внешнего мира (пользователь, файл, сеть), оно может быть каким угодно. Тут default — ваш план Б:

switch (cmd) {
    case 0: return 0;
    case 1: /* ... */ break;
    default:
        std::cout << "Unknown command: " << cmd << "\n";
        break;
}

Когда default мешает (особенно с enum class)

Если switch по enum class должен покрывать все варианты, default может спрятать ошибку: вы добавили новый элемент в enum class, а switch забыли обновить — но код продолжит компилироваться и «молча» уйдёт в default.

Поэтому для функций вроде ToString(Status) часто делают switch без default, перечисляя все case. А после switch оставляют страховочный return:

std::string ToString(Status s) {
    switch (s) {
        case Status::Todo:       return "todo";
        case Status::InProgress: return "in_progress";
        case Status::Done:       return "done";
    }
    return "unknown"; // сюда “не должны” попадать, но компилятору нужен return
}

9. Scope в switch

Самая неприятная ловушка switch: case не создаёт новый блок команд, но при этом внутри case вам часто хочется объявить переменные.

Новичок пишет так:

switch (cmd) {
    case 1:
        std::string title = ReadTitle(); // может привести к ошибке компиляции
        break;
    case 2:
        break;
}

И компилятор может выдать что-то вроде «crosses initialization of …». Смысл: switch — это прыжки по меткам, и компилятор не любит ситуации, когда можно «перепрыгнуть» инициализацию переменной.

Лечение простое: блок кода с фигурными скобками в case

switch (cmd) {
    case 1: {
        std::string title = ReadTitle();
        // ...
        break;
    }
    case 2:
        break;
}

9.2. И ещё одна «скрытая» проблема: переобъявление имён

Без блоков весь switch — один scope. Поэтому так тоже плохо:

switch (cmd) {
    case 1:
        int x = 1;
        break;
    case 2:
        int x = 2; // ошибка: x уже объявлен в этом scope
        break;
}

И опять спасают { ... }.

10. Практика: делаем меню TaskTracker на switch

Наша цель здесь не «написать идеальный трекер задач». Цель — аккуратно подключить меню, чтобы дальше (в следующей лекции) вы смогли спокойно реализовать CRUD‑операции над std::vector<Task> и не утонуть в if/else.

Модель задачи (мы её уже знаем)

#include <string>

enum class Status { Todo, InProgress, Done };

struct Task {
    int id;
    std::string title;
    Status status = Status::Todo;
};

Меню

#include <iostream>

void PrintMenu() {
    std::cout
        << "1) Add\n"
        << "2) List\n"
        << "3) Done\n"
        << "0) Exit\n";
}

Маленькая утилита: «съесть» остаток строки после чтения числа

Она нужна, чтобы после std::cin >> cmd следующий std::getline не схватил пустую строку.

#include <iostream>
#include <limits>

void EatLine() {
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

Скелет main: цикл + switch

Обратите внимание: case 0 делает return 0; так мы выходим из программы без флагов и лишних условий.

#include <iostream>
#include <vector>

int main() {
    std::vector<Task> tasks;
    int nextId = 1;

    while (true) {
        PrintMenu();

        int cmd = -1;
        if (!(std::cin >> cmd)) return 0;
        EatLine();

        switch (cmd) {
            case 0: return 0;
            case 1: /* Add */  break;
            case 2: /* List */ break;
            case 3: /* Done */ break;
            default:
                std::cout << "Unknown command\n";
                break;
        }
    }
}

Реализуем Add и List прямо в case

Add: здесь нужны локальные переменные → ставим блок { ... }

case 1: { // Add
    std::cout << "Title: ";
    std::string title;
    std::getline(std::cin, title);

    tasks.push_back(Task{nextId, title, Status::Todo});
    ++nextId;
    break;
}

List: просто выводим все задачи

(Пока печатаем максимально просто, красивый единый формат будем наводить позже.)

case 2: { // List
    for (const Task& t : tasks) {
        std::cout << t.id << ": " << t.title << "\n";
    }
    break;
}

Done: читаем id и меняем статус (черновик)

case 3: { // Done
    std::cout << "Id: ";
    int id = 0;
    std::cin >> id;
    EatLine();

    for (Task& t : tasks) {
        if (t.id == id) t.status = Status::Done;
    }
    break;
}

Да, тут много «наивного» (например, мы не сообщаем, если id не найден). Это нормально: сегодня мы тренируем switch и структуру меню. В следующей лекции мы вынесем операции в функции и сделаем их аккуратнее (по‑взрослому: найти, проверить, обновить).

11. Типичные ошибки при работе со switch

Ошибка №1: забыли break и получили «выполнилось сразу два case».
switch выполняется сверху вниз, пока его не остановить. Если в ветке нет break или return, то программа спокойно «провалится» дальше. Это один из самых частых багов, потому что код выглядит логично, а ведёт себя странно. Полезная привычка: когда пишете case, сразу же ставьте break, а потом заполняйте код между ними.

Ошибка №2: намеренный fallthrough сделали, но не пометили [[fallthrough]].
Даже если вы уверены, что «так и задумано», следующий читатель (включая вас через месяц) почти наверняка решит, что это ошибка. Компилятор тоже часто так решает и предупреждает. Атрибут [[fallthrough]]; — это честная табличка «здесь проваливаемся специально».

Ошибка №3: объявили переменную внутри case, но забыли {} и получили странную ошибку компиляции.
case не создаёт scope, и компилятор может ругаться на «прыжок через инициализацию». Если в ветке появляются локальные переменные, самый надёжный и читаемый стиль — case X: { ... break; }.

Ошибка №4: добавили default в switch по enum class и случайно спрятали баг.
Когда enum class расширится новым значением, ваш switch может перестать покрывать все варианты. Если есть default, код продолжит компилироваться и «молча» уйдёт в запасную ветку. Иногда это правильно, но иногда вы хотите, чтобы компилятор и вы сами заметили: «эй, тут появился новый вариант, реши, что с ним делать».

Ошибка №5: пытаются сделать switch по std::string.
Так нельзя: switch в C++ работает по целым/enum типам. Если команды строковые, сначала преобразуйте строку в enum class Command (через if/else), и уже по Command делайте switch.

1
Задача
C++ SELF, 19 уровень, 2 лекция
Недоступна
День недели
День недели
1
Задача
C++ SELF, 19 уровень, 2 лекция
Недоступна
Символьный калькулятор
Символьный калькулятор
1
Задача
C++ SELF, 19 уровень, 2 лекция
Недоступна
Префикс лога
Префикс лога
1
Задача
C++ SELF, 19 уровень, 2 лекция
Недоступна
Команды счётчика
Команды счётчика
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ