1. Вступ
Коли ви пишете першу сотню рядків коду, здається, що керування потоком зводиться лише до if, else і циклів. return виглядає формальністю: «ну треба ж щось повернути з int-функції». Але згодом раптом зʼясовується: половина плутанини у вкладених if/else зникає, якщо ви навчитеся виходити раніше з функції, коли продовжувати вже немає сенсу.
Варто запамʼятати просту річ: return негайно завершує поточну функцію. Не «після циклу», не «після if», не «колись потім». Просто зараз. Тому return — це інструмент не лише «для результату», а й «для структури» коду: він допомагає зробити читання лінійним, як інструкцію, а не перетворює код на лабіринт.
Уявіть, що функція — це невеликий сценарій. Якщо в ньому щось пішло не так (невалідне введення, порожній список, неправильна команда), то природна реакція — не тягнути всю функцію через десяток else, а спокійно сказати: «Гаразд, на цьому завершуємо».
2. Два види return: з виразом і без
Зараз акуратно розкладемо форми return по поличках. Це як із дверима: одні двері — «вихід із багажем», інші — «вихід без багажу». Якщо переплутати, буде або помилка компіляції, або дивна спроба проштовхнути валізу у вузькі дверцята. У C++ є форми return expr; і return;; вибір залежить від типу значення, яке повертає функція.
Міні-таблиця, яку зручно тримати в голові:
| Тип функції | Дозволений 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, подумки ставте там крапку й не плануйте «ще один рядок».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ