Попытка приблизить C++ к гарантиям Rust


В последний год мне пришлось временно перейти на Rust. Сейчас этот период закончился, и я с радостью возвращаюсь к своему любимому C++.

Пока я работал с Rust, многое меня раздражало, и я ждал возвращения к C++. Но после возвращения мне стало не хватать некоторых вещей из Rust. В первую очередь это сильная модель безопасности памяти и строгие проверки компилятора, которые Rust даёт практически из коробки. Не уверен, что в C++ можно полностью достичь того же уровня гарантий, но мне захотелось заметно к нему приблизиться.

Поэтому я настроил для проекта строгую сборку и несколько уровней проверок прямо в CI: диагностику на этапе компиляции, динамический анализ санитайзерами во время тестов и статический анализ исходного кода.

Текущий стек

  • ОС: Ubuntu 24.04
  • Компилятор: Clang 23
  • Система сборки: CMake + Ninja
  • CI: GitLab CI, сборка и запуск внутри Docker

Устройство пайплайна

GitLab-CI

  • check_commit_format — проверка формата коммитов;
  • check_linear_history — проверка линейности истории;
  • debug — отладочная сборка с прогоном тестов;
  • release — релизная сборка с прогоном тестов;
  • asan — прогон с AddressSanitizer, LeakSanitizer и UndefinedBehaviorSanitizer, включёнными в одной конфигурации;
  • tsan — отдельный прогон с ThreadSanitizer;
  • coverage — сборка с измерением покрытия кода тестами;
  • lifetime — анализ времени жизни объектов от llvm;
  • tidy — статический анализ;
  • format — проверка форматирования.

Санитайзеры

На уровне CI добавлены два прогона с санитайзерами:

  • asan — объединяет три инструмента в одной конфигурации:
    • AddressSanitizer — поиск выхода за границы памяти, use-after-free и double-free;
    • LeakSanitizer — поиск утечек памяти;
    • UndefinedBehaviorSanitizer — обнаружение поддерживаемых компилятором классов неопределённого поведения;
  • tsan — ThreadSanitizer, поиск гонок данных в многопоточном коде.

ASan, LSan и UBSan хорошо сочетаются в одной сборке, а вот ThreadSanitizer несовместим с AddressSanitizer, поэтому TSan вынесен в отдельную конфигурацию.

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

Диагностика компилятора

Также я усилил диагностику на этапе компиляции.

Базовые предупреждения

  • -Wall — основной набор предупреждений;
  • -Wextra — дополнительные общие предупреждения;
  • -Wpedantic — диагностика отклонений от стандарта C++;
  • -Wshadow — обнаружение затенения имён;
  • -Wnon-virtual-dtor — проверка полиморфных классов без виртуального деструктора;
  • -Woverloaded-virtual — скрытие виртуальных функций в производном классе;
  • -Wnull-dereference — потенциальные разыменования null;
  • -Wformat=2 — расширенная проверка форматных строк;
  • -Wimplicit-fallthrough — неявное прохождение между ветками switch;
  • -Wcast-align — потенциально небезопасное выравнивание после приведения типов;
  • -Werror=return-type — отсутствие корректного возвращаемого значения считается ошибкой;
  • -Werror=unused-result — игнорирование значимого результата считается ошибкой.

Строгий профиль

  • -Wconversion — потенциально опасные неявные преобразования;
  • -Wsign-conversion — преобразования между знаковыми и беззнаковыми типами;
  • -Wold-style-cast — использование C-style cast вместо C++-приведений;
  • -Wdouble-promotion — неявное продвижение float до double;
  • -Wfloat-conversion — потенциально опасные преобразования floating-point типов;
  • -Wfloat-equal — прямое сравнение чисел с плавающей точкой;
  • -Wundef — использование неопределённых макросов в условной компиляции;
  • -Wswitch-enum — необработанные значения перечислений в switch;
  • -Wextra-semi — лишние точки с запятой.

Дополнительные проверки Clang

  • -Wthread-safety — статическая проверка аннотаций потокобезопасности;
  • -Wmismatched-tags — несовпадение class, struct и union в объявлениях.

Режим ошибок

  • -Werror — превращение предупреждений в ошибки сборки (включаю его в CI-конфигурациях).

Анализ времени жизни

Отдельно добавил анализ времени жизни объектов:

  • -Wlifetime-safety — поиск потенциально висячих ссылок и указателей.

Это относительно новый анализ Clang и его набор диагностик зависит от версии компилятора, поэтому он вынесен в отдельный профиль lifetime. Он ловит определённые классы ошибок, связанных со временем жизни объектов. При разработке этого анализатора авторы вдохновлялись Polonius — механизмом проверки заимствований (borrow checker) в Rust (документация Clang).

Статический анализ и форматирование

  • Clang-Tidy — статический анализ и поиск потенциальных дефектов;
  • Clang-Format — единый стиль кода. В CI он работает как проверка, а не как автоматическое исправление: если код отформатирован не по правилам, пайплайн падает.

Покрытие

Сборка coverage собирает проект, прогоняет тесты и публикует отчёты (JUnit, Cobertura, HTML). Процент покрытия показывается в списке jobs, а в merge request GitLab подсвечивает покрытые и непокрытые строки изменённого кода.

Итог

Получившийся набор не превращает C++ в Rust и не даёт тех же гарантий безопасности памяти. Но набор позволяет ловить существенно больше ошибок на этапе компиляции, во время тестов и при статическом анализе.