Загружаем лабораторные…
// C++ · Лекция 05

Ошибки и неопределённое поведение

Как компилятор читает программу, почему код падает и чем опасно UB.

Стадии программы и виды ошибок

Когда программа ведёт себя неправильно, сначала полезно определить, на каком этапе возникла проблема. Если исполняемый файл ещё не получен, искать причину в введённых пользователем числах рано. Если программа уже запустилась, сообщение об ошибке компиляции от прошлой сборки может вообще не относиться к текущему запуску. Разделение на этапы помогает выбрать правильный следующий шаг, а не менять код наугад.

Три разные ситуации

Ошибка до запуска, авария во время выполнения и неопределённое поведение — не одно и то же. Причём UB может проявиться без видимого падения.

От исходника к работающей программе
01 / исходникКод C++Текст с объявлениями, выражениями, управляющими конструкциями
02 / проверкиКомпиляцияЛексика → синтаксис → семантика; затем генерация машинного кода
03 / сборкаКомпоновкаОбъектные файлы и библиотеки связываются в программу
04 / работаВыполнениеПрограмма читает данные и выполняет действия

Упрощённая схема: реальные этапы компилятора устроены сложнее и могут перекрываться.

СитуацияКогдаЧто наблюдаем
CE — Compilation Error
При компиляции исходного кода
Диагностика; исполняемый файл этой сборки не создан
Ошибка компоновки
При связывании объектных файлов
Например, undefined reference или multiple definition
RE — Runtime Error
Программа запустилась
Аварийное завершение, исключение без обработчика, сигнал ОС
UB — Undefined Behavior
Корректность программы нарушена
Никаких требований к результату: от «всё работает» до неожиданной оптимизации

Ошибка — это не всегда падение

CE обнаруживается до запуска; RE — наблюдаемая проблема во время исполнения. UB описывает отсутствие гарантий стандарта: оно может привести к RE, но может остаться незаметным.

Ошибка компоновки выделена отдельно: эта стадия рассматривается отдельно.

Не путай понятия
Выход за границыНарушение требований языка: UB
≠
Segmentation faultОдин из возможных исходов
UB без паденияUB с падениемUB с ошибочным результатом

Compile time (время компиляции) — Этап подготовки программы к исполнению: проверки корректности и преобразование исходного кода в машинный.

Runtime (время выполнения) — Период, когда уже запущенная программа выполняет машинные инструкции.

Следите, чтобы после неудачной сборки вы случайно не запускали старый исполняемый файл. Иначе легко получить обманчивую картину: исходник изменён, а поведение осталось прежним. Полезная последовательность для учебного примера — сохранить файл, убедиться в успешной сборке, запустить именно полученную программу и отдельно проверить её результат. При этом даже успешная сборка означает не больше, чем прохождение выполненных компилятором проверок.

Предупреждения компилятора

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

Warning — предупреждение, а не ошибка языка

Компилятор заметил подозрительное, но допустимое действие. Например, if (x = 0) синтаксически корректно, но чаще программист хотел сравнить x == 0.

Зачем нужны предупреждения?

Они дают шанс найти ошибку до запуска. В лекции отдельно обсуждаются бесполезные выражения, случайное присваивание вместо сравнения и подозрительные конверсии.

-Werror превращает включённые предупреждения в ошибки сборки.

Диагностика компилятора
$ g++ -Wall -Wextra main.cpp
warning: suggest parentheses around assignment
$ g++ -Wall -Wextra -Werror main.cpp
error: suggest parentheses around assignment
ФлагНазначение
-Wall
Широкий базовый набор предупреждений. Название не означает буквально все возможные.
-Wextra
Дополнительный набор предупреждений поверх базовых.
-Wpedantic
Предупреждает о ряде расширений, нарушающих выбранный ISO-стандарт C++.
-Werror
Считает включённые warnings ошибками компиляции.
-Wno-unused-value
Выключает один конкретный класс предупреждений (пример, не универсальная рекомендация).
-Werror=unused-value
Делает выбранный вид предупреждения ошибкой.

Флаг -Werror не добавляет новые правила C++ и не превращает компилятор в доказательство правильности программы. Он задаёт более строгую политику сборки: выбранные предупреждения необходимо устранить, прежде чем появится новый исполняемый файл. Если сообщение непонятно, лучше уменьшить пример и выяснить причину, чем сразу выключать соответствующий класс предупреждений для всего проекта.

Конструктор флагов компиляции
$ g++ -Wall -Wextra -Wpedantic main.cpp
warning: parentheses around assignment
-Werror не блокирует этот пример

Учебная симуляция: точные категории предупреждений и умолчания зависят от компилятора и его версии.

warnings.cpp
int main() {
    int x = 1;
    // if (x = 0) {}    // допустимо, но подозрительно
    // x + 5;           // результат выражения не используется
    if (x == 0) {}     // сравнение вместо присваивания
}

Не стоит отключать все предупреждения ради успешной сборки. Сначала исправляй код. Целенаправленное подавление уместно, когда причина полностью понятна.

Warning (предупреждение) — Диагностическое сообщение компилятора о потенциальной проблеме. Само по себе не требует отклонить программу.

Практика диагностики

Отладка становится спокойнее, когда у вас есть воспроизводимый пример: конкретная версия исходника, команда сборки, входные данные и ожидаемый результат. Это позволяет менять по одной причине за раз и понимать, что повлияло на поведение. Фраза «иногда ломается» менее полезна, чем маленький пример, который показывает проблему при каждом повторении.

Надёжная базовая команда

Для учебных программ удобно явно указывать стандарт языка и включать основные предупреждения. С флагом -Werror Важно о -Wall

Флаг -Wall включает типичный базовый набор предупреждений, а не все доступные проверки. Нередко стоит добавить -Wextra и -Wpedantic. Конкретные warnings зависят от GCC или Clang.
их придётся устранять до запуска.

При отладке помогает сборка с символами -g; режим -O2 полезен для оптимизированной версии, но меняет отладочный опыт.

Рекомендуемая компиляция
g++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -g main.cpp -o app
$ ./app

Уменьшайте программу постепенно: уберите несущественный вывод, сократите набор данных, оставьте только подозрительную операцию. После каждого изменения проверяйте, сохранилась ли проблема. Если ошибка исчезла, последнее удалённое действие может подсказать, какие условия были важны. Для границ контейнера, например, полезнее три элемента и один неверный индекс, чем большой файл с тысячами несвязанных операций.

build.sh
# Собрать учебный пример на C++20
# g++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -g main.cpp -o app
# ./app
Чек-лист при появлении проблемы
01Найди первую диагностику
02Отличи CE от RE
03Проверь границы, деление, инициализацию
04Воспроизведи на маленьком примере
Практический принцип

Ценность строгой компиляции в том, что проблемы обнаруживаются заранее. Однако успешная компиляция не доказывает отсутствия UB: компилятор обязан диагностировать не все некорректные случаи.

Diagnostics (диагностика) — Сообщения компилятора и инструментов о возможных или обязательных ошибках, с указанием места и часто предлагаемого исправления.

Разобранный пример и самостоятельная проверка

Начните отладку с точного вопроса: какой элемент требуется прочитать и какие индексы существуют? Вектор values содержит три значения, поэтому допустимы номера ноль, один и два. Если запрос равен размеру, подходящего элемента нет. Не нужно запускать чтение за границей и ждать падения: исправление состоит в проверке условия до обращения. В безопасной программе плохой доступ оставлен только комментарием, а выполняемый код сообщает об ошибочном индексе. Сначала запишите ожидаемый исход для каждого теста, затем запускайте программу. Проверка индекса один должна дать двадцать; индекса три — отказ. Если защита случайно использует знак больше вместо больше или равно, именно второй тест обнаружит ошибку. Предупреждения компилятора помогают, но не доказывают, что проверка написана правильно. Тест следует сохранить после исправления, чтобы та же граница не сломалась при следующем изменении программы.

code-guided-diagnostic.cpp
#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    const std::vector<int> values{10, 20, 30};
    const std::size_t index = 3;
    // values[index]; // Не выполнять при index >= values.size().
    if (index >= values.size()) {
        std::cerr << "Invalid index\n";
        return 1;
    }
    std::cout << values[index] << '\n';
}
Безопасный отказ до доступа: Invalid index; код завершения 1.

Один законченный сеанс исправления

  1. Симптом: для трёх элементов код пытается прочитать индекс 3. Ожидается отказ.
  2. Гипотеза: неверная граница проверки. Плохую операцию не выполняем.
  3. Минимальный вход: три значения и индекс 3; обычный контроль — индекс 1.
  4. Исправление: до доступа проверить index >= values.size().
  5. Повторный тест: 0, size−1 и size; затем пустой набор.

Санитайзеры: g++ -std=c++20 -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer main.cpp -o app. Они помогают на выполненных путях, но не доказывают отсутствие всех UB и не гарантируют обнаружение любого неинициализированного чтения.

Как компилятор понимает код

Сообщение компилятора — это попытка объяснить, на каком правиле языка остановился разбор программы. Он не знает вашу задумку, поэтому описывает фактическую запись. Например, пропущенная скобка может изменить структуру всего последующего текста. Тогда ошибка будет отмечена ниже настоящей причины, а исправление первой проблемы уберёт сразу несколько следующих сообщений.

Три уровня понимания текста

Компилятор должен сперва распознать токены Токен и лексема

Токен — категория значимого фрагмента (identifier, integer literal, оператор). Лексема — фактически написанные символы, например count или 123. На начальном уровне оба слова часто используют почти как синонимы.
, затем определить структуру программы и наконец проверить смысл операций. Эти уровни помогают правильно читать сообщения диагностики.

Лексический анализ

Разбивает поток символов на токены — идентификаторы, ключевые слова, операторы, скобки, литералы.

Например, int x = 5; можно представить как пять токенов.

Цепочка токенов
intx=5;

Пробелы отделяют части текста, но сами обычно не становятся токенами.

Синтаксический анализ

Проверяет, можно ли собрать из токенов конструкцию по грамматике C++.

У x = 5 + ; после бинарного плюса отсутствует правое выражение.

Дерево выражения
=x+5?

Семантический анализ

Конструкция синтаксически понятна, но компилятор проверяет, допустима ли операция для этих типов, объявлены ли имена, однозначна ли перегрузка.

std::string s; s * 3; — понятное выражение, но нужного умножения нет.

Форма понятна — смысл неверен
std::string × intДля std::string нет подходящего оператора умножения

Начинайте с первого содержательного сообщения и смотрите не только на выделенную строку, но и немного выше. Спросите себя: узнаваемы ли отдельные символы и токены, правильно ли собрана конструкция, допустимы ли операции для данных типов? Такое разделение не обязано точно повторять устройство конкретного компилятора, но помогает превратить длинный текст диагностики в несколько проверяемых вопросов.

Диагностику компилятора проще разбирать на небольшой программе с одной причиной ошибки. Рабочие версии приведены ниже; строки с ошибками оставлены в комментариях. Включайте их по одной и начинайте чтение с первого сообщения компилятора.

Пройдём стадии разбора
int x = 5;
intx=5;
Распознано пять отдельных токенов.

На этом шаге лексер ещё не проверяет значения переменных.

diagnostics.cpp
#include <string>
int main() {
    int x = 3;
    // x = 5 + ;                // синтаксическая ошибка
    // std::string s = "abc";
    // auto y = s * 3;          // семантическая ошибка
    // unknown_name = 1;       // имя не объявлено
    return x;
}

Сообщения компилятора не обязаны дословно совпадать между GCC и Clang. У них могут отличаться и формулировки, и точное место указания ошибки.

Токен (token) — Минимальная единица, которую лексер распознал в исходном тексте: идентификатор, оператор, литерал или знак пунктуации.

Семантическая ошибка (semantic error) — Код удаётся разобрать по грамматике, но действие невозможно по правилам языка или недопустимо для указанных сущностей.

Ошибки во время выполнения

Во время выполнения программа получает конкретные значения, которых могло не быть в ваших первых проверках. Массив оказывается пустым, индекс — слишком большим, делитель — нулём. Поэтому проверять следует не только «обычный» пример, но и границы допустимых данных. Важен сам нарушенный договор операции (какие условия должны были выполняться), а не только название сообщения, показанного операционной системой.

Программа запустилась, но завершилась аварийно

Runtime error — общее название проблем, возникающих после запуска. ОС и стандартная библиотека могут завершить программу, но механизм зависит от причины и платформы.

Segmentation fault Почему не каждый выход за границы вызывает segfault?
Защита памяти обычно работает с более крупными областями — страницами. Обращение к чужому элементу может попасть в отображённую страницу и внешне «сработать», хотя по стандарту уже произошло UB.

На системах с защитой памяти возникает при недопустимом обращении к адресному пространству: например, при обращении к неотображённой странице или попытке записи в память только для чтения.

Выход за границы vector через [] — UB, и segfault лишь один из возможных эффектов.

Память процесса
·······××
v[50000] может попасть в недоступную память — но падение не гарантировано.

Integer division by zero

int a = 5 / x; при x == 0 — UB в C++. На типичных Linux/x86 системах такой код может завершиться сигналом SIGFPE с надписью Floating point exception.

Название сигнала историческое: он может быть вызван и целочисленным делением.

Типичные сообщения терминала
$ ./program
Floating point exception (core dumped)
$ ./program
Segmentation fault (core dumped)

Необработанное исключение

v.at(i) проверяет границы. При неверном индексе метод выбрасывает std::out_of_range.

Если исключение не перехватить, вызывается std::terminate. Его стандартный обработчик обычно вызывает std::abort.

Проверяемое и непроверяемое
v[i]За границей — UB; ошибка не обязана обнаружиться
v.at(i)За границей — исключение std::out_of_range
bounds.cpp
#include <vector>
#include <stdexcept>
#include <iostream>
int main() {
    std::vector<int> v = {10, 20, 30};
    // v[3] = 5;          // UB: вне диапазона
    try {
        std::cout << v.at(3);
    } catch (const std::out_of_range&) {
        std::cout << "Неверный индекс";
    }
}

Сравните обращение values.at(index) с values[index]. В первом случае ошибка индекса сообщает о себе через предусмотренное библиотекой исключение, которое можно обработать. Во втором выход за допустимые границы не превращается автоматически в такое же исключение. Если программа «случайно не упала», это ещё не делает чтение корректным: нужно проверить индекс до обращения или выбрать операцию с проверкой.

Успешная компиляция не гарантирует, что данные подходят программе. Проверяйте внешние значения и границы контейнеров в местах использования. Во вкладках показаны два способа обработать ошибку и продолжить работу предсказуемо.

Что будет при обращении по индексу?
102030
Успешно: индекс 2 существует
30результат

Размер равен 3, индексы 0, 1, 2.

Segmentation fault (SIGSEGV) — Сигнал о нарушении доступа к памяти на соответствующих ОС. Не тождественен всякому выходу за границы массива.

Исключение (exception) — Механизм передачи управления обработчику ошибки через throw/catch. Не равно сигналу SIGFPE.

Неопределённое поведение

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

Неопределённое поведение (UB)

Если программа попадает в ситуацию, для которой стандарт C++ не предъявляет требований к поведению, дальнейший результат не гарантируется. Даже если программа «работала вчера», это не доказательство корректности.

Главная ловушка: ожидать падения

UB не обязано приводить к RE. Оно может выдавать правдоподобные числа, оставаться незаметным годами, менять результат после обновления компилятора или проявляться только при -O2.

Смысл прост: компилятор оптимизирует корректные программы, исходя из того, что нарушений требований языка нет.

Что разрешает UB
Кажется, работаетНеправильное обращение не привело к видимой проблеме
Неверный ответРезультат отличается от ожиданий
АварияОС завершает процесс
Иная оптимизацияВыполнение может кардинально измениться
ПримерПочему UBВозможный эффект
v[v.size()]
Обращение за последним элементом через []
Произвольное значение, падение или иное
int x; cout << x;
Чтение неинициализированного int (обычные случаи в контексте лекции)
Неопределённый результат
INT_MAX + 1
Переполнение знакового int
Оптимизатор может изменить логику
i++ + i++
Изменения i не упорядочены
Никакой гарантии значения

Для беззнаковых целочисленных типов Как устроено переполнение unsigned?

Если тип содержит N битов, арифметика с беззнаковым результатом ведётся по модулю 2^N. Например, для 8-битного unsigned char значение 255 + 1 после приведения к этому типу даст 0.
переполнение определено по модулю 2^N. Для знаковых целых — UB. Не переноси правило unsigned на обычный int.

Например, у вектора с тремя элементами допустимы обычные индексы от нуля до двух. Обращение к четвёртому элементу не становится безопасным оттого, что рядом с буфером оказалась доступная память. Чтобы исправить программу, нужно восстановить правильную границу обхода или изменить логику хранения. Перехватывать только видимое падение недостаточно: некорректное действие могло остаться незаметным.

undefined.cpp
#include <climits>
#include <vector>
void demo() {
    int x = INT_MAX;
    // ++x;                   // UB: signed overflow
    int i = 0;
    // int z = i++ + i++;     // UB: неупорядоченные изменения
    std::vector<int> v(3);
    // int a = v[3];          // UB: индекс вне границ
}

Undefined Behavior (UB) — Отсутствие требований стандарта к поведению при выполнении определённого некорректного действия; результат нельзя обосновывать наблюдением одного запуска.

Другие категории поведения

Не всякий результат, отличающийся на двух компьютерах, означает неопределённое поведение. Иногда стандарт специально оставляет реализации выбор из нескольких вариантов. Важно понять границы этого выбора: разрешено ли несколько конкретных исходов, обязан ли компилятор документировать решение или программа вообще нарушила требования языка. Эти ситуации требуют разных выводов о переносимости кода.

Не все варианты поведения — ошибка

Если стандарт допускает несколько ограниченных исходов, это ещё не UB. Нужно различать unspecified, implementation-defined и ill-formed, no diagnostic required.

Если порядок двух допустимых независимых действий не указан, нельзя требовать один заранее выбранный порядок, но и говорить «разрешено абсолютно всё» неверно. Если свойство определяется реализацией, его можно узнать из документации используемых инструментов и платформы. А при UB документация об одном наблюдаемом запуске не заменяет исправления программы. Сначала назовите категорию, затем решайте, на что можно опираться.

Какую гарантию даёт стандарт?
Нет требований к результатуКод некорректен; исход не ограничен
Примерint x = INT_MAX; ++x;
Нельзя строить ожидания на основе отдельного запуска.
КатегорияЧто гарантированоПример
Undefined behavior
Гарантий для поведения не устанавливается
Знаковое переполнение
Unspecified behavior
Возможен один из разрешённых вариантов; реализация не обязана заранее сообщать выбор
Порядок вычисления независимых аргументов функции
Implementation-defined
Реализация выбирает допустимый вариант и документирует его
Размер int на конкретной платформе
IFNDR
Программа некорректна, но реализация не обязана выдавать диагностику
Некоторые нарушения ODR между единицами трансляции

Для f(a(), b()) в C++17 порядок аргументов не указан, но вычисления одного аргумента не перемешиваются с другим. Сам по себе такой вызов — не UB.

Unspecified Behavior (неуказанное поведение) — Стандарт разрешает несколько определённых вариантов поведения, не требуя документировать конкретный выбор.

Implementation-defined Behavior (поведение, определяемое реализацией) — Реализация выбирает и документирует один из разрешённых стандартом вариантов.

IFNDR (ill-formed, no diagnostic required) — Нарушение правила языка, которое реализация не обязана диагностировать. Важно не рассчитывать на корректную работу даже после успешной сборки.

Почему UB меняет оптимизацию

Оптимизатор не обязан механически переводить каждую строку исходника в отдельную машинную команду. Он может сокращать работу, используя свойства языка и типов. Это нормально для корректной программы: внутренние шаги меняются, а требуемый наблюдаемый результат сохраняется. Проблема появляется, когда программист рассчитывает на поведение, которое стандарт для его кода не обещает.

Почему оптимизация меняет результат

Компилятор вправе рассуждать так: «Если корректная программа не переполняет int, то определённые условия всегда истинны». Это может убрать проверки, на которые программист рассчитывал при переполнении.

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

Пример из лекции: i * 12345678

При типичном 32-битном int максимальное значение — 2 147 483 647. Пока i ≤ 173, умножение помещается. При i = 174 происходит знаковое переполнение.

Без оптимизации можно случайно увидеть отрицательное число. При -O2 в конкретном примере лекции исчезла проверка, и цикл стал бесконечным.

Знаковое переполнение нельзя надёжно обнаружить сравнением уже вычисленной суммы: ошибка произошла бы раньше проверки. Сначала сравните операнды с границами типа. Беззнаковая арифметика имеет определённый переход через границу, но он тоже может нарушить логику задачи.

Граница signed int
173
173i
2 135 802 294математическое произведение
Результат помещается в типичный 32-битный signed int.
overflow.cpp
#include <iostream>
#include <limits>
int main() {
    for (int i = 173; i <= 174; ++i) {
        const long long product = 1LL * i * 12'345'678;
        std::cout << i << ": " << product;
        if (product > std::numeric_limits<int>::max()) {
            std::cout << " outside int";
        }
        std::cout << '\n';
    }
    // int overflow = 174 * 12'345'678; // Не выполнять: UB для 32-bit int.
}
Вычисление выполняется в long long до проверки границы int. Это математическое произведение, а не результат выполнения UB.
Как оптимизатор мог рассуждать
1Корректная программа не переполняет знаковый int
2Следовательно, произведение определено только для i ≤ 173
3В таком случае i < 300 гарантированно истинно
4Проверка представляется избыточной и может быть устранена

Это объяснение возможной оптимизации, а не обещание, что любой компилятор обязательно создаст бесконечный цикл.

Правило as-if Что считается наблюдаемым поведением?
Это не просто «всё, что мы видим на экране»: учитываются требования абстрактной машины C++, в том числе наблюдаемые операции ввода-вывода и доступы к volatile-объектам. Ненаблюдаемые промежуточные шаги оптимизатор может менять.

Оптимизатор может перестраивать программу как угодно, если для корректного кода сохраняется предписанное стандартом наблюдаемое поведение.

Именно поэтому исходный порядок инструкций не всегда совпадает с машинным. Но вызовы ввода-вывода и другие наблюдаемые эффекты нельзя менять произвольно.

Два способа исполнения
Исходная программавычисление → вычисление → выводstd::cout << 12;
⇄
Оптимизированнаялишние шаги сокращеныstd::cout << 12;
Наблюдаемый вывод тот же для корректной программы.

Оптимизация (optimization) — Преобразование кода с целью эффективности при сохранении требуемого наблюдаемого поведения корректной программы.

As-if rule (правило «как если бы») — Разрешает реализации разные внутренние преобразования, пока наблюдаемое поведение корректной программы остаётся допустимым.

+Дополнительно

Санитайзеры: поиск ошибок во время запуска

В самой лекции санитайзеры только упоминаются как будущая тема. Это дополнительная полезная практика: AddressSanitizer помогает обнаруживать некоторые ошибки доступа к памяти, UndefinedBehaviorSanitizer — ряд вариантов UB.

sanitizers.sh
# Дополнительная проверка для отладки (GCC / Clang)
# g++ -std=c++20 -g -O1 -fsanitize=address,undefined \
#     -fno-omit-frame-pointer main.cpp -o app
# ./app

Ни один санитайзер не гарантирует обнаружение всех UB. Инструменты могут пропускать ошибки, особенно если соответствующий путь исполнения не был проверен.

Разбор вывода предупреждений

file.cpp:12:5 показывает файл, строку и столбец; warning: обозначает категорию диагностики; [-Wunused-value] — имя конкретного класса предупреждений GCC/Clang.

Тонкости std::abort

Исключение std::out_of_range, выброшенное vector::at, можно перехватить с помощью catch. Если оно остаётся необработанным, вызывается std::terminate, и стандартный обработчик обычно завершает процесс через abort. Нельзя считать это неизбежным выводом «Aborted» во всех окружениях.

Почему нельзя судить об UB по одному запуску?

Исполняемый код зависит от компилятора, версии, флагов и платформы. «У меня сработало» не является гарантией стандарта языка.

Термины

Compilation error

Исходный код не проходит обязательные проверки компиляции.

unknown_name();

Примеры кода

Диагностика до опасного доступа

checked-index-cases.cpp
#include <iostream>
#include <vector>
int main() {
    const std::vector<int> values{10, 20, 30};
    int index = 0;
    if (!(std::cin >> index) || index < 0 ||
        static_cast<std::size_t>(index) >= values.size()) {
        std::cerr << "Invalid index\n";
        return 1;
    }
    std::cout << values[static_cast<std::size_t>(index)] << '\n';
}

Безопасный отказ до доступа: Invalid index; код завершения 1.

code-guided-diagnostic.cpp
#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    const std::vector<int> values{10, 20, 30};
    const std::size_t index = 3;
    // values[index]; // Не выполнять при index >= values.size().
    if (index >= values.size()) {
        std::cerr << "Invalid index\n";
        return 1;
    }
    std::cout << values[index] << '\n';
}

Обсудить лекцию с ИИ

Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.

Посмотреть промпт