Попытка приблизить C++ к гарантиям Rust
В последний год мне пришлось временно перейти на Rust. Сейчас этот период закончился, и я с радостью возвращаюсь к своему любимому C++.
Пока я работал с Rust, многое меня раздражало, и я ждал возвращения к C++. Но после возвращения мне стало не хватать некоторых вещей из Rust. В первую очередь это сильная модель безопасности памяти и строгие проверки компилятора, которые Rust даёт практически из коробки. Не уверен, что в C++ можно полностью достичь того же уровня гарантий, но мне захотелось заметно к нему приблизиться.
Поэтому я настроил для проекта строгую сборку и несколько уровней проверок прямо в CI: диагностику на этапе компиляции, динамический анализ санитайзерами во время тестов и статический анализ исходного кода.
Текущий стек
- ОС: Ubuntu 24.04
- Компилятор: Clang 23
- Система сборки: CMake + Ninja
- CI: GitLab CI, сборка и запуск внутри Docker
Устройство пайплайна

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 и не даёт тех же гарантий безопасности памяти. Но набор позволяет ловить существенно больше ошибок на этапе компиляции, во время тестов и при статическом анализе.