CMake для pet-проекта на C++ #
Эта статья расскажет, как поэтапно настроить CMake в новом проекте на C++.
- Предполагаем, что это pet-проект, в котором не нужны ни кросс-компиляция, ни поддержка старых версий инструментов.
- Если вы знакомы с CMake, но не вполне представляете его современные возможности, то можете прочитать мою статью Современный CMake: 10 советов по улучшению скриптов сборки.
Всё, что описано ниже, актуально на март 2026 года.
Начальные условия #
- Используем C++23
- Используем на разработку для Linux, с компиляцией через GCC или Clang
- Поддержка Windows, MSVC, Mac OS X — на втором плане
Допустим, структура проекта такова:
- Есть статическая библиотека в каталоге
src/libsupport - Есть тесты для неё в каталоге
tests/libsupport_tests - В каждом проекте и в корневом каталоге есть файл
CMakeLists.txt
То же самое в ASCII-графике:
.
├── CMakeLists.txt
├── src/
│ └── libsupport/
│ └── CMakeLists.txt
└── tests/
└── libsupport_tests/
└── CMakeLists.txtНастраивать CMake будем поэтапно, добавляя одну возможность за другой.
Корневой CMakeLists.txt #
Пример:
cmake_minimum_required(VERSION 3.31)
project(petproject LANGUAGES CXX)
enable_testing()
find_package(Catch2 3 REQUIRED)
# Подключаем каталоги модулей этого проекта.
add_subdirectory(src/libsupport)
# Подключаем каталоги тестов этого проекта.
add_subdirectory(tests/libsupport_tests)Объяснение:
- Наша конфигурация требует CMake 3.31 или выше
- CMake 3.31 выпущен ноября 2024: https://www.kitware.com/cmake-3-31-0-available-for-download/
- Установить новый CMake на Ubuntu можно так, как описано на askubuntu: How do I install the latest version of cmake from the command line?
- Наш проект называется “petproject”
- Мы используем “ctest” для запуска тестов
- Мы используем библиотеку Catch3 для написания тестов
CMakeLists.txt для статической библиотеки #
Пример файла src/libsupport/CMakeLists.txt:
add_library(
support
FileUtil.h
FileUtil.cpp
)
# Заголовки библиотеки доступны другим проектам.
target_include_directories(
support
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)Здесь мы определили сборку статической библиотеки “libsupport” (цель сборки support), в которой есть только два файла с исходным кодом:
- “FileUtil.h”
- “FileUtil.cpp”
CMakeLists.txt для тестов #
Пример файла tests/libsupport_tests/CMakeLists.txt:
add_executable(
libsupport_tests
FileUtilTest.cpp
)
target_link_libraries(libsupport_tests PRIVATE support Catch2::Catch2WithMain)
add_test(libsupport_tests libsupport_tests)Здесь мы определили сборку исполняемого файла “libsupport_test” с единственным файлом исходного кода “FileUtilTest.cpp” и зарегистрировали его как тест.
Команды CMake для сборки #
Лично я добавляю в корень проекта Makefile, чтобы не запоминать наизусть команды CMake.
Допустим, нам нужны следующие цели для make:
| Цель | Предназначение |
|---|---|
make make | Генерация конфигурации сборки из CMakeLists.txt |
make build | Сборка из исходников |
make test | Запуск тестов |
make clean | Очистка продуктов сборки |
Для реализации можно написать такой Makefile:
# Use `make BUILD_TYPE=Release` to change build type
BUILD_TYPE = Debug
all: cmake build test
cmake:
cmake CMakeLists.txt -Bbuild/$(BUILD_TYPE) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE)
build:
cmake --build build/$(BUILD_TYPE) --parallel
test: build
ctest --output-on-failure --test-dir build/$(BUILD_TYPE)
clean:
git clean -fdx build/$(BUILD_TYPE)
.PHONY: cmake build test cleanПри желании вы можете расширить этот Makefile:
- добавить цель
make format, чтобы вызывать clang-format - добавить цель
make lint, чтобы вызывать clang-tidy или другой линтер
Поддержка редактора VSCode #
Если вы используете для разработки Visual Studio Code с официальным плагином “C/C++ Extension Pack”, то можно попросить CMake экспортировать compile_commands.json и сообщить об этом редактору VSCode.
В корневой CMakeLists.txt добавьте:
# Экспортируем команды для использования с VSCode C++ IntelliSense
# См. https://stackoverflow.com/questions/54671883/how-can-i-set-up-c-c-intellisense-for-a-cmake-project-in-vs-code
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)Затем создайте файл .vscode/settings.json и добавьте в него:
{
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/Debug/compile_commands.json",
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools"
}Выше мы предполагаем, что вы используете для разработки Debug конфигурацию и сборка находится в каталоге build/Debug/.
Также стоит установить плагин “CMake Tools”, если он ещё не установлен.
Включаем поддержку C++23 #
В теории включить C++23 для конкретной цели сборки в современном CMake очень просто:
target_compile_features(${TARGET} PRIVATE cxx_std_23)На практике может потребоваться ещё и вручную установить флаги компиляции, чтобы нужный стандарт точно был включён для всех версий CMake и компиляторов.
Для этого создадим в проекте файл cmake/cpp23.cmake:
# Функция custom_enable_cpp23 включает стандарт C++23 для выбранной цели.
function(custom_enable_cpp23 TARGET)
message(STATUS "Enabling C++23 for target: ${TARGET}")
target_compile_features(${TARGET} PRIVATE cxx_std_23)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
target_compile_options(${TARGET} PRIVATE -std=c++23)
elseif(MSVC)
target_compile_options(${TARGET} PRIVATE /std:c++latest)
endif()
set_target_properties(${TARGET} PROPERTIES
CXX_STANDARD 23
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
endfunction()Подключите его в корневом CMakeLists.txt:
# Подключаем CMake-модули данного проекта
include(cmake/cpp23.cmake)Теперь в каждом подпроекте для соответствующей цели сборки вызовите эту функцию. Например, в src/libsupport/CMakeLists.txt можно добавить:
custom_enable_cpp23(libsupport)Включаем поддержку Sanitizers #
В современном C++ при разработке всегда стоит включать санитайзеры (sanitizers):
- Санитайзеры — это встроенные в основные компиляторы C/C++ средства инструментирования кода, позволяющие при выполнении обнаруживать ошибки работы с памятью, состояния гонки между потоками, undefined behavior и другие вещи
- Для GCC/Clang мы будем использовать два санитайзера: address sanitizer и undefined behavior sanitizers
- Для сборки через MSVC мы будем требовать как минимум Visual Studio 2019 и использовать только address sanitizer
Создайте в проекте файл cmake/sanitizers.cmake:
# Функция custom_enable_sanitizers включает sanitizers для выбранной цели.
function(custom_enable_sanitizers TARGET)
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
# Для Clang и GCC используем встроенные санитайзеры:
# - address sanitizer
# - undefined behavior sanitizer
target_compile_options(${TARGET} PRIVATE
-fsanitize=address,undefined
-fno-omit-frame-pointer
)
target_link_options(${TARGET} PRIVATE
-fsanitize=address,undefined
)
elseif(MSVC)
# MSVC имеет встроенные санитайзеры через /fsanitize (новые версии)
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.28)
target_compile_options(${TARGET} PRIVATE /fsanitize=address)
else()
message(FATAL_ERROR "Sanitizers require MSVC 2019 16.9+")
endif()
endif()
endif()
endfunction()Подключите его в корневом CMakeLists.txt:
# Подключаем CMake-модули данного проекта
include(cmake/cpp23.cmake)
include(cmake/sanitizers.cmake)Теперь в каждом подпроекте для соответствующей цели сборки вызовите эту функцию. Например, в src/libsupport/CMakeLists.txt можно добавить санитайзеры вместе с поддержкой C++23:
custom_enable_cpp23(support)
custom_enable_sanitizers(support)Включаем режим строгих предупреждений #
Предупреждения в коде — это вещь, которую не стоит игнорировать для нового кода. Тем более, что компиляторы C/C++ обычно выдают предупреждения по делу.
Хороший подход выглядит так:
- включить весь набор полезных предупреждений, поддерживаемых компилятором
- установить режим “warning as error”, чтобы предупреждения роняли сборку проекта
Реализуем это, добавив в проект файл cmake/warnings.cmake:
# Функция custom_enable_strict_warnings включает режим строгих предупреждений для выбранной цели
function(custom_enable_strict_warnings TARGET)
message(STATUS "Enabling strict warnings for target: ${TARGET}")
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
target_compile_options(${TARGET} PRIVATE
-Wall
-Wextra
-Wpedantic
-Wshadow
-Wconversion
-Wsign-conversion
-Wnull-dereference
-Wdouble-promotion
-Wformat=2
-Wundef
-Werror
)
elseif(MSVC)
target_compile_options(${TARGET} PRIVATE
/W4 # Максимальный уровень предупреждений
/WX # Обрабатывать предупреждения как ошибки
/permissive- # Строгое соответствие стандартам
)
endif()
set_target_properties(${TARGET} PROPERTIES
COMPILE_WARNING_AS_ERROR ON
)
endfunction()В корневом CMakeLists.txt подключим этот файл:
# Подключаем CMake-модули данного проекта
include(cmake/cpp23.cmake)
include(cmake/sanitizers.cmake)
include(cmake/warnings.cmake)Далее добавим вызов custom_enable_strict_warnings для каждой цели.
В примере показано подключение C++23, санитайзеров и строгого режима предупреждений для цели “support”:
custom_enable_cpp23(support)
custom_enable_sanitizers(support)
custom_enable_strict_warnings(support)Как подавлять лишние предупреждения #
По возможности предупреждение лучше исправить сразу. Но есть особые случаи:
- неиспользуемые параметры
- предупреждения, возникшие из-за подключения заголовочных файлов
- ложные предупреждения
Подавление неиспользуемых параметров выполняется конструкцией (void)name;. Пример:
void AbstractPass::visit(ast::LiteralExpression& e)
{
(void)e;
}Для GCC/Clang подавление предупреждений из заголовочных файлов выполняется с помощью #pragma GCC diagnostic:
#pragma GCC diagnostic pushдобавляет текущее состояние флагов диагностики в стек#pragma GCC diagnostic ignored "-W..."подавляет предупреждение, задаваемое каким-либо флагом вида-W...#pragma GCC diagnostic popвосстанавливает состояние флагов диагностики из стека
Пример:
// Подавляем предупреждения из заголовочных файлов LLVM.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wconversion"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include <llvm/IR/Constant.h>
#include <llvm/IR/Constants.h>
#include <llvm/IR/Verifier.h>
#pragma GCC diagnostic popАналогично выполняется подавление ложных срабатываний:
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnull-dereference"
std::string support::readTextFile(const std::filesystem::path& path)
{
std::ifstream file(path);
if (!file.is_open())
{
throw std::filesystem::filesystem_error(
"Cannot read file",
path,
std::error_code(errno, std::generic_category()));
}
return std::string(std::istreambuf_iterator<char>(file), {});
}
#pragma GCC diagnostic pop