JavaRush /Курси /C++ SELF /return і ранній вихід

return і ранній вихід

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

1. Вступ

Коли ви пишете першу сотню рядків коду, здається, що керування потоком зводиться лише до if, else і циклів. return виглядає формальністю: «ну треба ж щось повернути з int-функції». Але згодом раптом зʼясовується: половина плутанини у вкладених if/else зникає, якщо ви навчитеся виходити раніше з функції, коли продовжувати вже немає сенсу.

Варто запамʼятати просту річ: return негайно завершує поточну функцію. Не «після циклу», не «після if», не «колись потім». Просто зараз. Тому return — це інструмент не лише «для результату», а й «для структури» коду: він допомагає зробити читання лінійним, як інструкцію, а не перетворює код на лабіринт.

Уявіть, що функція — це невеликий сценарій. Якщо в ньому щось пішло не так (невалідне введення, порожній список, неправильна команда), то природна реакція — не тягнути всю функцію через десяток else, а спокійно сказати: «Гаразд, на цьому завершуємо».

2. Два види return: з виразом і без

Зараз акуратно розкладемо форми return по поличках. Це як із дверима: одні двері — «вихід із багажем», інші — «вихід без багажу». Якщо переплутати, буде або помилка компіляції, або дивна спроба проштовхнути валізу у вузькі дверцята. У C++ є форми return expr; і return;; вибір залежить від типу значення, яке повертає функція.

Міні-таблиця, яку зручно тримати в голові:

Тип функції Дозволений return Що означає
int, bool, std::string, ...
return expr;
Повернути значення й завершити функцію
void
return;
(або просто дійти до кінця)
Просто завершити функцію без значення

return expr; — повертаємо значення

#include <iostream>

int square(int x) {
    return x * x;
}

int main() {
    std::cout << square(7) << '\n'; // 49
}

Тут усе просто: функція обіцяла int, отже має повернути int.

return; — ранній вихід із void-функції

#include <iostream>

void print_positive(int x) {
    if (x <= 0) return;      // ранній вихід
    std::cout << x << '\n';  // друкуємо лише якщо x > 0
}

int main() {
    print_positive(-5);
    print_positive(10);      // 10
}

Зверніть увагу: return; тут — це не «повернути порожнечу як значення», а просто «вийти — і все».

3. «Усі шляхи мають повертати значення»

Наступна проблема дуже часто трапляється в новачків: функція начебто повертає int, і десь усередині ви вже написали return, але компілятор однаково бурчить. Чому? Тому що компілятор, а разом із ним — і ви, намагаються зрозуміти, що станеться на кожному можливому шляху виконання. Якщо існує шлях, на якому функція може дійти до кінця без return, це проблема.

Інтуїтивна аналогія така: ви підписали контракт «я завжди віддаю посилку», а в одному сценарії просто мовчки йдете зі складу. Клієнт (компілятор) нервує — і цілком слушно.

Ось поганий приклад:

#include <string>

int score_to_grade(int score) {
    if (score >= 90) return 5;
    if (score >= 70) return 4;
    if (score >= 50) return 3;
    // а якщо score дорівнює 10?..
}

Правильний варіант — явно закрити випадок, що залишився:

int score_to_grade(int score) {
    if (score >= 90) return 5;
    if (score >= 70) return 4;
    if (score >= 50) return 3;
    return 2; // усе, що нижче 50
}

Тут немає жодної магії: ми просто чесно кажемо, що робити в інших випадках.

Іноді студент намагається «виправити» це так: додає гілки else і робить структуру глибшою. Але часто значно простіше лишити лінійний ланцюжок і поставити фінальний return внизу. Це як просто зачинити двері, а не будувати ще один коридор.

4. Ранній вихід: guard clauses

Зараз ми підходимо до головного прийому цієї лекції: раннього виходу, який часто називають guard clause, тобто захисною перевіркою. Це техніка, у якій ви спочатку відсікаєте «погані» випадки, а вже потім пишете основну логіку. Для початківців це дуже зручний стиль: він перетворює код на послідовність перевірок, а не на піраміду з if/else.

Порівняйте два стилі. Перший — «ліс із if»:

#include <iostream>

void print_discounted_price(int price, int discount) {
    if (price > 0) {
        if (discount >= 0 && discount <= 100) {
            int result = price - price * discount / 100;
            std::cout << result << '\n';
        } else {
            std::cout << "Некоректна знижка\n";
        }
    } else {
        std::cout << "Некоректна ціна\n";
    }
}

Тут начебто все коректно, але читається важкувато: щоб дістатися до суті, треба пройти крізь два рівні умов.

Тепер той самий зміст, але з ранніми виходами:

#include <iostream>

void print_discounted_price(int price, int discount) {
    if (price <= 0) {
        std::cout << "Некоректна ціна\n";
        return;
    }
    if (discount < 0 || discount > 100) {
        std::cout << "Некоректна знижка\n";
        return;
    }
    int result = price - price * discount / 100;
    std::cout << result << '\n';
}

Код став помітно прозорішим: спочатку — «охорона на вході», потім — звичайний сценарій.

Невелика блок-схема того, що відбувається в guard-style:

flowchart TD
    A[Початок функції] --> B{price > 0?}
    B -- ні --> X[Повідомляємо про помилку] --> R[return]
    B -- так --> C{discount у межах 0..100?}
    C -- ні --> Y[Повідомляємо про помилку] --> R
    C -- так --> D[Обчислюємо результат] --> E[Виводимо результат] --> F[Кінець]

Зауважте важливу деталь: guard clauses особливо добре працюють у функціях, які перевіряють вхідні дані. А в реальному житті вхідні дані майже ніколи не бувають ідеальними.

5. return у циклах і пошуку

Ще одне місце, де return спрощує код, — це пошук усередині циклу. Початківці часто думають так: «створю прапорець found, а після циклу перевірю його». Це робочий підхід, але він нерідко робить код довшим і додає зайві змінні.

Якщо задачу можна сформулювати як «знайди й повідом результат», природне рішення просте: щойно знайшли — одразу return.

Приклад: чи містить рядок цифру

#include <string>

bool contains_digit(std::string s) {
    for (char c : s) {
        if (c >= '0' && c <= '9') return true;
    }
    return false;
}

Якщо цифра трапляється на початку рядка, ми не зобовʼязані проходити його до кінця «заради порядку». Ми вже знаємо відповідь.

Приклад: знайти нотатку за назвою

Домовмося, що ми пишемо простий консольний застосунок NoteKeeper. Він зберігає нотатки в std::vector<std::string>, уміє додавати їх, виводити список і видаляти за назвою. Це не «ідеальний продукт», але чудовий навчальний полігон.

Функція пошуку індексу:

#include <string>
#include <vector>

int find_note_index(const std::vector<std::string>& notes, std::string title) {
    for (int i = 0; i < static_cast<int>(notes.size()); ++i) {
        if (notes[i] == title) return i;
    }
    return -1;
}

Тут return використано саме так, як треба: знайшли — повернули індекс; не знайшли — повернули -1 наприкінці. Так, тут є static_cast<int>, щоб уникнути конфлікту типів (різницю між int і size_t ми вже обговорювали й ще не раз побачимо на практиці).

6. NoteKeeper: збираємо застосунок із ранніми return

Зараз ми зробимо невеликий крок до цілісного прикладу: поєднаємо ідеї return і раннього виходу в один міні-застосунок. Наша мета — щоб код читався як сценарій: «показати меню → прочитати команду → виконати → повторити». І щоб кожна функція була максимально простою: перевірила вхідні дані, якщо щось не так — вийшла, якщо все гаразд — виконала роботу.

main() і код завершення програми

main() — це теж функція, просто «особлива»: її результат — це код завершення програми. Неформально це можна уявляти так: 0 означає «усе добре», а ненульове значення — «щось пішло не так».

У навчальних задачах ви майже завжди повертаєте 0. Іноді ви побачите, що return 0; узагалі не пишуть — у C++ дозволено «дійти до кінця main», і це буде еквівалентно успішному завершенню.

Я все одно рекомендую спершу явно писати return 0;: так ви краще відчуєте, що main — звичайна функція, а завершення програми — усвідомлена точка.

Приклад із різними результатами:

#include <iostream>

int main() {
    int x = 0;
    std::cin >> x;

    if (x < 0) {
        std::cout << "Відʼємне значення недопустиме\n";
        return 1; // сигнал: помилка або неуспіх
    }

    std::cout << "OK\n";
    return 0; // успіх
}

Прототипи функцій

#include <string>
#include <vector>

void print_menu();
int read_command();
void list_notes(const std::vector<std::string>& notes);
void add_note(std::vector<std::string>& notes);
void remove_note(std::vector<std::string>& notes);
int find_note_index(const std::vector<std::string>& notes, std::string title);

Тут ми не обговорюємо організацію за файлами. Поки що все міститься в одному .cpp-файлі.

Меню та читання команди

#include <iostream>

void print_menu() {
    std::cout << "1) Список нотаток\n";
    std::cout << "2) Додати нотатку\n";
    std::cout << "3) Видалити нотатку\n";
    std::cout << "0) Вихід\n";
}

int read_command() {
    int cmd = -1;
    if (!(std::cin >> cmd)) return -1; // якщо зчитування не вдалося, виходимо
    return cmd;
}

Зверніть увагу на стиль: якщо введення не вдалося, ми не продовжуємо робити вигляд, що команда є. Ми чесно повертаємо «погане значення» (-1). Так, пізніше ми зробимо введення ще надійнішим, але зараз нам важливо побачити силу раннього виходу.

Виведення списку нотаток

#include <iostream>
#include <vector>
#include <string>

void list_notes(const std::vector<std::string>& notes) {
    if (notes.empty()) {
        std::cout << "(порожньо)\n";
        return;
    }
    for (const std::string& n : notes) std::cout << n << '\n';
}

Якщо список порожній, основний сценарій — тобто цикл друку — просто не потрібен. Ранній return; робить це очевидним.

Додавання нотатки

#include <iostream>
#include <string>
#include <vector>

void add_note(std::vector<std::string>& notes) {
    std::string title;
    std::cout << "Назва: ";
    std::cin >> title;

    if (title.empty()) return; // у реальності сюди рідко потрапимо через >>
    notes.push_back(title);
}

Тут видно, що guard clauses — це насамперед звичка. Навіть якщо саме тут empty() майже не спрацює, такий спосіб мислення все одно корисний.

Видалення за назвою

#include <iostream>
#include <string>
#include <vector>

void remove_note(std::vector<std::string>& notes) {
    std::string title;
    std::cout << "Назва для видалення: ";
    std::cin >> title;

    int idx = find_note_index(notes, title);
    if (idx == -1) {
        std::cout << "Не знайдено\n";
        return;
    }
    notes.erase(notes.begin() + idx);
}

Тут ранній return; прибирає зайвий else і робить основний сценарій лінійним: знайшли — видалили.

main: сценарій, який легко читати

#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> notes;

    while (true) {
        print_menu();
        int cmd = read_command();

        if (cmd == 0) return 0;
        if (cmd == 1) list_notes(notes);
        else if (cmd == 2) add_note(notes);
        else if (cmd == 3) remove_note(notes);
        else std::cout << "Невідома команда\n";
    }
}

Зверніть увагу: return 0; усередині циклу — цілком нормальна конструкція. Ми не зобовʼязані робити break, потім виходити з циклу, а тоді ще десь щось повертати. Якщо «вихід із застосунку» — це справді кінець main, можна прямо так і написати.

7. Типові помилки під час роботи з return і раннім виходом

Помилка №1: забути return на одному зі шляхів у функції, яка має повернути значення.
Зазвичай це трапляється, коли ви написали if (умова) return ...;, а для інших випадків нічого не передбачили. Компілятор може видати попередження, а інколи поведінка стає просто непередбачуваною. Звичка проста: після ланцюжка умов завжди передбачайте «випадок за замовчуванням» фінальним return.

Помилка №2: писати return; у функції, яка повертає int/bool/std::string.
Іноді здається: «ну я ж хочу просто вийти раніше». Але контракт функції — повернути значення. Якщо вам потрібно вийти раніше, поверніть зрозуміле значення (наприклад, false, -1, порожній рядок) і домовтеся про це в дизайні функції.

Помилка №3: намагатися повернути значення з void-функції.
Це дзеркальна ситуація: void означає «значення немає». Якщо ви раптом ловите себе на бажанні написати return result;, отже функція насправді має повертати результат, і її тип варто змінити (або розділити на дві функції: «порахувати» і «надрукувати»).

Помилка №4: будувати глибокі if/else, хоча можна було додати кілька захисних перевірок і спокійно рухатися далі.
Найчастіше це виглядає як «пірамідка вправо»: код зсувається на 4 пробіли на кожному рівні, і за хвилину ви читаєте вже не логіку, а просто сходи. Ранні виходи — це чесний спосіб сказати: «якщо випадок поганий — завершуємо», і не тягнути його через усю функцію.

Помилка №5: писати код після return і дивуватися, чому він не виконується.
Після return функція завершилася. Усе, що нижче, стає недосяжним. Іноді компілятор навіть попереджає про unreachable code, але не завжди. Хороша звичка: якщо ви поставили return, подумки ставте там крапку й не плануйте «ще один рядок».

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