CMake для pet-проекта на C++

CMake для pet-проекта на C++ #

Эта статья расскажет, как поэтапно настроить CMake в новом проекте на C++.

  • Предполагаем, что это pet-проект, в котором не нужны ни кросс-компиляция, ни поддержка старых версий инструментов.
  • Если вы знакомы с CMake, но не вполне представляете его современные возможности, то можете прочитать мою статью Современный CMake: 10 советов по улучшению скриптов сборки.

Всё, что описано ниже, актуально на март 2026 года.

Начальные условия #

  1. Используем C++23
  2. Используем на разработку для Linux, с компиляцией через GCC или Clang
  3. Поддержка 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)

Объяснение:

  1. Наша конфигурация требует CMake 3.31 или выше
  2. Наш проект называется “petproject”
  3. Мы используем “ctest” для запуска тестов
  4. Мы используем библиотеку 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

Сайт atdd.ru — блог разработчика.