JavaRush /Курси /C++ SELF /switch у C++: розгалуження за значенням, break, fallthrou...

switch у C++: розгалуження за значенням, break, fallthrough

C++ SELF
Рівень 19 , Лекція 2
Відкрита

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 — це мітка, а не окремий блок

Працює це так:

  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: переходимо до наступного 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 Приклади
Цілі числа
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 << "Невідома команда\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]]?

Він робить дві корисні речі:

  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 << "Невідома команда: " << 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.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ