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 << "Bad discount\n";
        }
    } else {
        std::cout << "Bad price\n";
    }
}

Здесь вроде бы всё корректно, но читается тяжеловато: чтобы добраться до сути, нужно пройти два уровня условий.

Теперь тот же смысл, но с ранними выходами:

#include <iostream>

void print_discounted_price(int price, int discount) {
    if (price <= 0) {
        std::cout << "Bad price\n";
        return;
    }
    if (discount < 0 || discount > 100) {
        std::cout << "Bad discount\n";
        return;
    }
    int result = price - price * discount / 100;
    std::cout << result << '\n';
}

Смысл стал заметно прозрачнее: сначала «охрана на входе» (guards), потом нормальный сценарий.

Небольшая блок-схема того, что происходит в 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 << "Negative is not allowed\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) List notes\n";
    std::cout << "2) Add note\n";
    std::cout << "3) Remove note\n";
    std::cout << "0) Exit\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 << "(empty)\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 << "Title: ";
    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 << "Title to remove: ";
    std::cin >> title;

    int idx = find_note_index(notes, title);
    if (idx == -1) {
        std::cout << "Not found\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 << "Unknown command\n";
    }
}

Обратите внимание: return 0; внутри цикла — абсолютно нормальная конструкция. Мы не обязаны делать break, потом выходить из цикла, потом ещё где-то возвращать. Если «выход из приложения» — это действительно конец main, можно прямо так и написать.

7. Типичные ошибки при работе с return и ранним выходом

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

Ошибка №2: писать return; в функции, которая возвращает int/bool/string.
Иногда кажется: «ну я же хочу просто выйти пораньше». Но контракт функции — вернуть значение. Если вам нужно выйти раньше, верните понятное значение (например, false, -1, пустую строку) и договоритесь об этом в дизайне функции.

Ошибка №3: пытаться вернуть значение из void-функции.
Это зеркальная ситуация: void означает «значения нет». Если вы вдруг ловите себя на желании написать return result; — значит, функция на самом деле должна возвращать результат, и её тип стоит поменять (или разделить на две функции: «посчитать» и «напечатать»).

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

Ошибка №5: писать код после return и удивляться, почему он не работает.
После return функция закончилась. Всё, что ниже, станет недостижимым. Иногда компилятор даже предупреждает об unreachable code, но не всегда. Хорошая привычка: если вы поставили return, мысленно поставьте там точку и не планируйте «ещё одну строчку».

1
Задача
C++ SELF, 14 уровень, 3 лекция
Недоступна
Индикатор знака
Индикатор знака
1
Задача
C++ SELF, 14 уровень, 3 лекция
Недоступна
Проверка скидки
Проверка скидки
1
Задача
C++ SELF, 14 уровень, 3 лекция
Недоступна
Первый минус
Первый минус
1
Задача
C++ SELF, 14 уровень, 3 лекция
Недоступна
Оценка теста
Оценка теста
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ