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]] и почему»:
| Функция/тип | Что возвращает | Почему нельзя игнорировать |
|---|---|---|
|
|
Иначе вы продолжите работу с невалидными данными |
|
|
Иначе вы «распарсили в никуда» |
|
|
Иначе поиск был бессмысленным действием |
|
|
Иначе ошибка «теряется» и становится молчаливой |
4. Типичные ошибки при использовании [[nodiscard]]
Ошибка №1: думать, что [[nodiscard]] обрабатывает ошибку.
Этот атрибут не добавляет никаких if, не чинит ввод и не возвращает вас во времени до момента бага. Он всего лишь просит компилятор предупредить: «вы уверены, что не забыли обработать результат?» Обработку всё равно пишете вы: через if, ранний return, печать сообщения.
Ошибка №2: ставить [[nodiscard]] вообще на всё подряд.
Если пометить каждую вторую функцию, вы очень быстро привыкнете игнорировать предупреждения, а IDE превратится в красно-жёлтую ёлку. Хороший критерий простой: игнорирование результата должно быть почти всегда ошибкой. Если «иногда нормально» — скорее всего, [[nodiscard]] не нужен.
Ошибка №3: гасить предупреждение вместо того, чтобы исправить логику.
Иногда новичок видит warning, добавляет (void)func(); и радуется тишине. Но тишина не означает корректность. Приведение к void уместно только когда вы действительно уверены, что результат не нужен. В контексте парсинга, поиска и ввода это редкий случай.
Ошибка №4: пометить [[nodiscard]], но не продумать удобный контракт.
Если ваша функция возвращает bool, но не говорит, что именно пошло не так, вызывающий код может начать плодить странные «универсальные сообщения об ошибке». В рамках учебных примеров это решают минимально: либо возвращают optional, либо печатают понятный текст на месте, либо (в небольших случаях) возвращают простой OpStatus.
Ошибка №5: считать, что предупреждение будет одинаковым везде.
Разные компиляторы и настройки сборки ведут себя по-разному: где-то [[nodiscard]] строго подсветится, где-то мягко, а где-то предупреждения могут быть отключены. Поэтому относитесь к [[nodiscard]] как к второй линии защиты: первая линия — ваш аккуратный код с проверками после каждого потенциально неуспешного шага.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ