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 — это метка, а не отдельный блок
Механика такая:
- вычисляется выражение в switch (expr) (один раз);
- управление прыгает на подходящую метку case ...: (или default:);
- дальше выполнение идёт вниз, пока вы не остановите его 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 | Примеры |
|---|---|
| Целые числа | |
| Перечисления | |
| Нельзя напрямую switch | Что делать вместо этого |
|---|---|
|
обычно if/else (и осторожно с точностью) |
|
сначала распарсить в 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]]?
Он делает две полезные вещи:
- читателю кода видно, что break не забыли;
- компилятор (часто) перестаёт предупреждать «возможно забыли 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ