1. Вступ
Ви вже знайомі з оператором if/else. Він ідеально підходить, коли треба перевірити умови:
- число більше або менше (x > 10);
- рядок порожній (title.empty());
- кілька умов одночасно (a % 2 == 0 && b % 2 == 0).
Але інколи завдання має іншу природу: «Є одне значення, і воно дорівнює одному з варіантів — виберіть потрібну гілку».
Класичний приклад — команди меню:
if (cmd == 1) {
/* додати */
} else if (cmd == 2) {
/* список */
} else if (cmd == 3) {
/* виконано */
} else if (cmd == 0) {
/* вихід */
} else {
/* невідома команда */
}
Працює? Так. Читається? Якщо команд 3–4, теж так. Але коли їх стає 10, 15 або 20, ланцюжок перетворюється на «паровозик умов».
Оператор switch формулює ту саму думку простіше: «розгалужуємося за значенням».
switch (cmd) {
case 1: /* додати */ break;
case 2: /* список */ break;
case 3: /* виконано */ break;
case 0: /* вихід */ break;
default: /* невідома команда */ 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: переходимо до наступного case"]
D -- так --> F["виходимо зі switch"]
2.3. Fallthrough: «провалювання» в наступний case
Оператору switch уже багато десятиліть, тож він успадкував поведінку, яка колись здавалася зручною й логічною. За замовчуванням виконується не лише вибрана гілка, а й усі наступні після неї. Щоб зупинити виконання, потрібно написати break.
Найтиповіша ситуація:
switch (cmd) {
case 1:
std::cout << "Додати\n";
case 2:
std::cout << "Список\n";
}
Якщо cmd == 1, ви побачите і Додати, і Список. Тому що break забули.
Виправлення просте, але рятівне:
switch (cmd) {
case 1:
std::cout << "Додати\n";
break; // завершує switch
case 2:
std::cout << "Список\n";
break; // завершує switch
}
4. На що можна робити switch (а на що — ні)
У C++ оператор switch має доволі жорсткі обмеження. Для навчальної практики достатньо запамʼятати просте правило: у нього можна передавати лише дві категорії типів — цілі числа та перелічення.
| Можна для 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 << "Невідома команда\n";
break;
}
5. case має бути константою часу компіляції
Крім того, значення після 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 не є константою часу компіляції
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 << "Невідома команда: " << 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. Область видимості у 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 має одну область видимості. Тому так теж погано:
switch (cmd) {
case 1:
int x = 1;
break;
case 2:
int x = 2; // помилка: x уже оголошено в цій області видимості
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) Додати\n"
<< "2) Список\n"
<< "3) Виконано\n"
<< "0) Вийти\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: /* Додати */ break;
case 2: /* Список */ break;
case 3: /* Виконано */ break;
default:
std::cout << "Невідома команда\n";
break;
}
}
}
Реалізуємо Add і List прямо в case
Add: тут потрібні локальні змінні, тому ставимо блок { ... }
case 1: { // Додати
std::cout << "Назва: ";
std::string title;
std::getline(std::cin, title);
tasks.push_back(Task{nextId, title, Status::Todo});
++nextId;
break;
}
List: просто виводимо всі завдання
(Поки виводимо максимально просто; красивий єдиний формат наведемо пізніше.)
case 2: { // Список
for (const Task& t : tasks) {
std::cout << t.id << ": " << t.title << "\n";
}
break;
}
Done: читаємо id і змінюємо статус (чернетка)
case 3: { // Виконано
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 не створює нової області видимості, і компілятор може лаятися через «стрибок через ініціалізацію». Якщо в гілці зʼявляються локальні змінні, найнадійніший і найчитабельніший стиль — case X: { ... break; }.
Помилка № 4: додали default у switch для enum class і випадково приховали баг.
Коли enum class розшириться новим значенням, ваш switch може перестати покривати всі варіанти. Якщо є default, код продовжить компілюватися й «мовчки» піде в запасну гілку. Іноді це правильно, але іноді ви хочете, щоб і компілятор, і ви самі помітили: «ей, тут зʼявився новий варіант, вирішіть, що з ним робити».
Помилка № 5: намагаються використати switch для std::string.
Так не можна: switch у C++ працює з цілими числами та enum-типами. Якщо команди рядкові, спочатку перетворіть рядок на enum class Command (через if/else), а вже потім робіть switch для Command.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ