JavaRush /Курсы /C++ SELF /[[nodiscard]]: дисциплина не игнорировать результат

[[nodiscard]]: дисциплина не игнорировать результат

C++ SELF
20 уровень , 4 лекция
Открыта

1. Почему важно не игнорировать результат

Если бы программисты всегда читали код внимательно, мы бы все давно ушли в отпуск и жили на проценты от дивидендов, но реальность сурова: код читают по диагонали, особенно свой, особенно в режиме «я сейчас быстро поправлю». Именно поэтому ошибки типа «вызвал функцию, а результат не проверил» встречаются постоянно — и неприятны тем, что программа выглядит «почти работающей».

Представьте типичную ситуацию: вы написали парсер числа, он возвращает std::optional<int>. Вы вызываете его, но забываете присвоить результат в переменную (или присвоили, но не проверили), и дальше делаете вид, что всё хорошо. Это не «сложная ошибка C++», это чисто человеческая: забыли одну строчку. И вот тут [[nodiscard]] играет роль правила «пристёгивайтесь ремнём»: он не заменит мозг, но сильно уменьшит шанс улететь в кювет на первом повороте.

Атрибуты в C++: что такое [[...]] и почему компилятор вас слушает

Атрибуты в C++ — это такие «наклейки» на код: вы ничего не меняете в логике напрямую, но добавляете компилятору подсказку. Подсказка может быть мягкой или строгой: например, «эй, не игнорируй результат, это подозрительно». Синтаксис у них единый: двойные квадратные скобки [[...]]. Сегодня используем [[nodiscard]].

Важно понимать ожидание правильно: атрибут — это не гарантия, что компилятор сделает ровно одно и то же во всех средах. Обычно [[nodiscard]] выдаёт предупреждение (warning), а будет ли warning считаться ошибкой — зависит от настроек сборки. Но даже предупреждение — это уже огромная помощь: IDE подсветит, CI может «заругаться», а вы заметите проблему сразу, а не через три часа отладки.

Кстати, [[nodiscard]] — штука настолько практичная, что её активно используют в стандартной библиотеке. Но вокруг него периодически идут обсуждения «где оставить, где убрать»: инструмент сильный и полезный, но если применять слишком широко, он создаёт шум из предупреждений.

2. [[nodiscard]] в функциях и возвращаемых значениях

[[nodiscard]] на функции: сигнал «проверь меня»

Самый частый сценарий — пометить функцию, результат которой почти всегда нужно проверять. Обычно это функции, возвращающие bool («успех/неуспех»), и функции, возвращающие std::optional<T> («значение есть/нет»). Тогда, если вы вызвали функцию и выбросили результат, компилятор имеет полное моральное право спросить: «Вы уверены, что это не ошибка?»

Посмотрим на маленький пример. Мы пишем функцию чтения целого числа из std::cin. Сейчас нам важно другое: функция возвращает bool, и этот bool нельзя игнорировать.

#include <iostream>

[[nodiscard]] bool read_int(int& out) {
    return static_cast<bool>(std::cin >> out);
}

int main() {
    int x = 0;
    read_int(x);                 // <- компилятор может предупредить: результат проигнорирован
    std::cout << x << '\n';
}

Смысл простой: если read_int вернул false, переменная x может остаться со старым значением (или с нулём, как повезёт), и программа продолжит работу в «полусломанном» состоянии. [[nodiscard]] не даёт вам случайно сделать вид, что всё ок.

Теперь правильное использование выглядит так: сначала проверка, потом использование.

#include <iostream>

[[nodiscard]] bool read_int(int& out) {
    return static_cast<bool>(std::cin >> out);
}

int main() {
    int x = 0;
    if (!read_int(x)) {
        std::cout << "Bad input\n";   // Bad input
        return 0;
    }
    std::cout << "x=" << x << '\n';   // x=...
}

Обратите внимание на «психологию кода»: [[nodiscard]] подталкивает писать ветку обработки ошибки сразу, а не откладывать на потом.

[[nodiscard]] + std::optional: для парсеров и поиска

std::optional сам по себе уже делает отсутствие результата явным, но проблема остаётся: вы всё ещё можете случайно проигнорировать возвращаемый optional. Особенно когда пишете цепочки функций: «прочитал строку → распарсил число → нашёл элемент → изменил».

Начнём со строгого парсинга из строки (по мотивам темы про std::from_chars). Мы заворачиваем логику «строгое число без хвоста» в функцию и помечаем её [[nodiscard]].

#include <charconv>
#include <optional>
#include <string_view>
#include <system_error>

[[nodiscard]] std::optional<int> parse_int_strict(std::string_view sv) {
    int value = 0;
    auto res = std::from_chars(sv.data(), sv.data() + sv.size(), value);

    if (res.ec != std::errc{}) return std::nullopt;
    if (res.ptr != sv.data() + sv.size()) return std::nullopt;
    return value;
}

Почему это «must check»? Потому что иначе вы можете сделать вот так:

#include <string_view>

// parse_int_strict объявлена выше

int main() {
    parse_int_strict("123");  // <- вроде «вызвал», но ничего не сделал. Почти всегда баг.
}

Если вы видите такую строчку в проекте — это почти всегда след «программист отвлёкся на чай». [[nodiscard]] как раз про то, чтобы компилятор вернул вас из чайной обратно в код.

Правильное использование:

#include <iostream>
#include <string>

// parse_int_strict объявлена выше

int main() {
    std::string s;
    std::getline(std::cin, s);

    auto value = parse_int_strict(s);
    if (!value) {
        std::cout << "Not a strict int\n";   // Not a strict int
        return 0;
    }
    std::cout << "ok: " << *value << '\n';  // ok: ...
}

Точно так же удобно помечать [[nodiscard]] функции поиска. Раньше вы могли вернуть -1, теперь возвращаем optional<size_t>. Игнорировать индекс найденного элемента обычно бессмысленно, значит [[nodiscard]] уместен:

#include <cstddef>
#include <optional>
#include <vector>

[[nodiscard]] std::optional<std::size_t> find_index(const std::vector<int>& v, int value) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        if (v[i] == value) return i;
    }
    return std::nullopt;
}

Как осознанно игнорировать результат

Иногда результат действительно не нужен, и это нормально. Проблема в том, что «иногда» и «случайно забыл» выглядят одинаково: в обоих случаях результат проигнорирован. Поэтому в хороших кодовых базах принято: если вы игнорируете [[nodiscard]]-результат намеренно, сделайте это явно, чтобы читатель понял: «да, тут так задумано».

Самый простой способ — приведение к void. Это прямой сигнал: «я знаю, что возвращается значение, но я его сознательно выбрасываю».

#include <optional>
#include <string_view>

// parse_int_strict объявлена выше

int main() {
    (void)parse_int_strict("123"); // осознанно игнорируем (но, честно, зачем?)
}

Почему здесь возникает «но зачем?» Потому что в контексте ввода, парсинга и поиска игнорирование результата почти всегда означает логическую ошибку: вы вызвали функцию ради результата, а не ради побочного эффекта. И [[nodiscard]] как раз помогает отличить «опечатка/забыл» от «осознанно».

[[nodiscard]] на типах: когда полезно помечать целые «результаты операции»

[[nodiscard]] можно ставить не только на функции, но и на типы. Это удобно, когда вы создаёте маленький тип-результат: например, структура «успех + сообщение». Тогда вы хотите, чтобы любое создание такого объекта «просило» вызывающий код проверить, что там внутри.

В рамках учебных примеров можно сделать простую структуру для результата операции (без исключений и без expected):

#include <string>

struct [[nodiscard]] OpStatus {
    bool ok = false;
    std::string message;
};

Теперь функция может возвращать OpStatus, и если вы проигнорируете его — компилятор может предупредить.

#include <string>

struct [[nodiscard]] OpStatus {
    bool ok = false;
    std::string message;
};

[[nodiscard]] OpStatus validate_name(const std::string& name) {
    if (name.empty()) return {false, "Name is empty"};
    return {true, "ok"};
}

int main() {
    validate_name(""); // <- проигнорировали статус: подозрительно
}

Это полезно именно как дисциплина: вы заставляете себя (и будущего себя) писать код, где ошибки не прячутся «между строк».

3. Пример: [[nodiscard]] в мини-приложении TaskBox

Давайте соберём всё в одну понятную историю, чтобы это было не «разрозненные примеры», а ощущалось как реальный код. Пусть у нас есть консольное мини-приложение TaskBox: список задач, где мы можем добавить задачу и отметить её выполненной. Мы умеем хранить данные в std::vector, делать поиск и печать, а сегодня делаем ввод и парсинг более дисциплинированными.

Сначала зададим модель:

#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

Теперь напишем поиск задачи по id. Для новичка очень удобно возвращать не «сырой указатель» и не -1, а std::optional<std::size_t> — индекс или отсутствие. И это отличный кандидат на [[nodiscard]].

#include <cstddef>
#include <optional>
#include <vector>

[[nodiscard]] std::optional<std::size_t> find_task_index_by_id(
    const std::vector<Task>& tasks, int id)
{
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        if (tasks[i].id == id) return i;
    }
    return std::nullopt;
}

Дальше нам нужен строгий парсер id из строки, и мы хотим, чтобы никто не забыл проверить его результат:

#include <charconv>
#include <optional>
#include <string_view>
#include <system_error>

[[nodiscard]] std::optional<int> parse_int_strict(std::string_view sv) {
    int value = 0;
    auto res = std::from_chars(sv.data(), sv.data() + sv.size(), value);

    if (res.ec != std::errc{}) return std::nullopt;
    if (res.ptr != sv.data() + sv.size()) return std::nullopt;
    return value;
}

Теперь представим команду done 3, где 3 — id задачи. Мы читаем строку, разбиваем «на глаз», и дальше обязаны проверить оба результата: парсинг числа и поиск задачи.

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

// Task, find_task_index_by_id, parse_int_strict объявлены выше

void mark_done(std::vector<Task>& tasks, const std::string& arg) {
    auto id = parse_int_strict(arg);
    if (!id) {
        std::cout << "Bad id\n";               // Bad id
        return;
    }

    auto idx = find_task_index_by_id(tasks, *id);
    if (!idx) {
        std::cout << "Task not found\n";       // Task not found
        return;
    }

    tasks[*idx].done = true;
    std::cout << "Marked done\n";              // Marked done
}

Сейчас главный смысл даже не в функциональности, а в том, что код читается как цепочка контрактов: «если не распарсилось — выходим», «если не нашли — выходим», «если всё ок — делаем действие». И вот здесь [[nodiscard]] работает как страховка: если вы случайно забудете auto id = ... и напишете просто parse_int_strict(arg);, компилятор может вас остановить предупреждением.

Чтобы закрепить картину, вот маленькая таблица «что мы помечаем [[nodiscard]] и почему»:

Функция/тип Что возвращает Почему нельзя игнорировать
read_int(int&)
bool
Иначе вы продолжите работу с невалидными данными
parse_int_strict(string_view)
optional<int>
Иначе вы «распарсили в никуда»
find_task_index_by_id(...)
optional<size_t>
Иначе поиск был бессмысленным действием
OpStatus
ok + message
Иначе ошибка «теряется» и становится молчаливой

4. Типичные ошибки при использовании [[nodiscard]]

Ошибка №1: думать, что [[nodiscard]] обрабатывает ошибку.
Этот атрибут не добавляет никаких if, не чинит ввод и не возвращает вас во времени до момента бага. Он всего лишь просит компилятор предупредить: «вы уверены, что не забыли обработать результат?» Обработку всё равно пишете вы: через if, ранний return, печать сообщения.

Ошибка №2: ставить [[nodiscard]] вообще на всё подряд.
Если пометить каждую вторую функцию, вы очень быстро привыкнете игнорировать предупреждения, а IDE превратится в красно-жёлтую ёлку. Хороший критерий простой: игнорирование результата должно быть почти всегда ошибкой. Если «иногда нормально» — скорее всего, [[nodiscard]] не нужен.

Ошибка №3: гасить предупреждение вместо того, чтобы исправить логику.
Иногда новичок видит warning, добавляет (void)func(); и радуется тишине. Но тишина не означает корректность. Приведение к void уместно только когда вы действительно уверены, что результат не нужен. В контексте парсинга, поиска и ввода это редкий случай.

Ошибка №4: пометить [[nodiscard]], но не продумать удобный контракт.
Если ваша функция возвращает bool, но не говорит, что именно пошло не так, вызывающий код может начать плодить странные «универсальные сообщения об ошибке». В рамках учебных примеров это решают минимально: либо возвращают optional, либо печатают понятный текст на месте, либо (в небольших случаях) возвращают простой OpStatus.

Ошибка №5: считать, что предупреждение будет одинаковым везде.
Разные компиляторы и настройки сборки ведут себя по-разному: где-то [[nodiscard]] строго подсветится, где-то мягко, а где-то предупреждения могут быть отключены. Поэтому относитесь к [[nodiscard]] как к второй линии защиты: первая линия — ваш аккуратный код с проверками после каждого потенциально неуспешного шага.

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