EduBrick

Отладка: санитайзеры, 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 сохранит только ответ, а отладка останется на экране.

Порядок действий, когда что-то не работает

Дисциплина, которая экономит больше всего времени:

  1. Перечитать условие. Примерно каждая пятая «ошибка в коде» — это невнимательно прочитанное требование.
  2. Проверить сэмплы вручную, а не глазами по коду.
  3. Собрать с санитайзерами и прогнать на сэмплах.
  4. Написать стресс.
  5. И только теперь читать код.

Пятый пункт стоит последним не случайно: чтение собственного кода — самый медленный и самый ненадёжный способ найти в нём ошибку. Глаз видит то, что задумано, а не то, что написано.