Отладка: санитайзеры, assert и флаги
Как заставить компилятор показывать пальцем на строку с ошибкой вместо «ошибка исполнения» без объяснений.
4 мин
Вердикт «ошибка исполнения» не говорит ничего. Программа упала — где, почему, на каком индексе, неизвестно.
Локально это исправляется одной строкой при компиляции.
Строка компиляции, которую стоит запомнить
g++ -std=c++20 -g -fsanitize=address,undefined -D_GLIBCXX_DEBUG solution.cpp -o solution
Что делает каждый флаг:
| флаг | что ловит |
|---|---|
-g |
добавляет отладочные символы — без него не будет номеров строк |
-fsanitize=address |
выход за границы массива, чтение освобождённой памяти, утечки |
-fsanitize=undefined |
переполнение знакового целого, сдвиг на отрицательное, деление на ноль |
-D_GLIBCXX_DEBUG |
проверки внутри контейнеров STL: v[i] за границей, неверный итератор |
Так выглядит результат на программе с двумя ошибками сразу:
solution.cpp:6:22: runtime error: signed integer overflow:
1000000 * 1000000 cannot be represented in type 'int'
Error: attempt to subscript container with out-of-bounds index 7,
but container only holds 5 elements.
Номер строки, номер столбца, конкретные значения. Разница с «ошибкой исполнения» — как между «где-то поломалось» и готовым исправлением.
Важно: эти флаги замедляют программу в несколько раз и на сервер их отправлять не нужно. Это инструмент локальной отладки.
Про _GLIBCXX_DEBUG отдельно
Санитайзер адресов не поймает выход за границы vector, если вы вышли недалеко: память там всё ещё выделена, просто не ваша по смыслу. Обращение v[7] при размере 5 читает соседние байты той же аллокации, и с точки зрения ASan ничего не произошло.
-D_GLIBCXX_DEBUG заменяет реализацию контейнеров на версию с проверками — и такие обращения ловятся. Замедление при этом серьёзное, зато находится целый класс ошибок, который иначе проявляется только на закрытых тестах.
Альтернатива без перекомпиляции всего — писать v.at(i) вместо v[i] в подозрительных местах: at проверяет границы всегда.
assert
assert(условие) — проверка, которая роняет программу с понятным сообщением, если условие ложно.
#include <cassert>
assert(l <= r); // инвариант бинарного поиска
assert(0 <= index && index < n); // индекс в диапазоне
assert(answer >= 0); // ответ не может быть отрицательным
Расставлять их стоит там, где вы уверены в утверждении. Смысл не в проверке ввода, а в фиксации собственных предположений: если предположение оказалось неверным, вы узнаете об этом сразу и в нужном месте, а не тремя функциями позже.
Особенно полезно внутри проверки в поиске по ответу и внутри рекурсии — там ошибка иначе всплывает далеко от причины.
На сервере assert можно оставить: он стоит одно сравнение. Но если он сработает, вердикт будет «ошибка исполнения», а не «неверный ответ». Иногда это как раз удобно — вы отличаете «моя логика сломалась» от «я неправильно решил задачу».
Отключается всё разом определением NDEBUG перед включением заголовка.
Условная компиляция для отладочного вывода
Отладочная печать, которую не нужно вычищать перед отправкой:
#ifdef LOCAL
#define debug(x) cerr << #x << " = " << (x) << '\n'
#else
#define debug(x)
#endif
debug(l);
debug(r - l);
Компилируете локально с -DLOCAL — печать есть. На сервере флага нет, макрос разворачивается в пустоту, накладных расходов ноль.
Конструкция #x превращает выражение в строку, поэтому debug(r - l) напечатает r - l = 3. Мелочь, но при десятке значений в выводе она экономит много внимания.
Почему cerr, а не cout
Отладочный вывод должен идти в cerr. Три причины:
- в интерактивных задачах
coutчитает судья, и ваша отладка станет запросом; cerrне буферизуется, поэтому при падении вы увидите всё, что успело напечататься;- потоки разделены, и
./solution > out.txtсохранит только ответ, а отладка останется на экране.
Порядок действий, когда что-то не работает
Дисциплина, которая экономит больше всего времени:
- Перечитать условие. Примерно каждая пятая «ошибка в коде» — это невнимательно прочитанное требование.
- Проверить сэмплы вручную, а не глазами по коду.
- Собрать с санитайзерами и прогнать на сэмплах.
- Написать стресс.
- И только теперь читать код.
Пятый пункт стоит последним не случайно: чтение собственного кода — самый медленный и самый ненадёжный способ найти в нём ошибку. Глаз видит то, что задумано, а не то, что написано.