Введение
По мере роста Qt-проекта единый монолитный CMakeLists.txt со всеми исходниками становится неудобен: компиляция замедляется, границы между подсистемами размываются, а повторное использование кода в других проектах требует копирования файлов. Решение — разбить проект на модули (библиотеки), каждый со своим CMakeLists.txt, и связать их через target_link_libraries. В статье разберём, как организовать многомодульный Qt-проект и в чём разница между PUBLIC, PRIVATE и INTERFACE зависимостями.
Концепция
Современный CMake (с версии 3.x, target-based подход) описывает зависимости не как глобальные флаги компилятора, а как свойства конкретных целей (targets). Когда модуль A линкуется с модулем B через target_link_libraries(A PRIVATE B), заголовки и флаги B становятся видны при компиляции A, но не «протекают» дальше — в код, который линкуется с A. PUBLIC означает, что зависимость нужна и для компиляции самого A, и распространяется на всех, кто линкуется с A. INTERFACE — зависимость не нужна для компиляции A самого, но обязательна для тех, кто его использует (типичный случай для header-only библиотек или модулей, которые просто агрегируют флаги).
Правильный выбор видимости — PRIVATE по умолчанию, и только при реальной необходимости PUBLIC — снижает связанность модулей и ускоряет повторную сборку при изменении внутренних деталей.
Пример кода: CMake
# core/CMakeLists.txt — базовая библиотека без UI-зависимостей
add_library(AppCore STATIC
src/datamodel.cpp
src/datamodel.h
)
target_include_directories(AppCore PUBLIC include)
target_link_libraries(AppCore PUBLIC Qt6::Core)
# network/CMakeLists.txt — модуль сети, зависит от AppCore
add_library(AppNetwork STATIC
src/apiclient.cpp
src/apiclient.h
)
target_link_libraries(AppNetwork
PUBLIC AppCore
PRIVATE Qt6::Network
)
# ui/CMakeLists.txt — модуль интерфейса, видит AppCore и AppNetwork,
# но детали реализации сети (Qt6::Network) ему PRIVATE и не нужны
add_library(AppUi STATIC
src/mainwindow.cpp
src/mainwindow.h
)
target_link_libraries(AppUi
PUBLIC AppCore
PRIVATE AppNetwork Qt6::Widgets
)
# Главный CMakeLists.txt — собирает исполняемый файл
add_executable(MyApp src/main.cpp)
target_link_libraries(MyApp PRIVATE AppUi)
Пояснения к коду
AppCore объявляет Qt6::Core как PUBLIC — это означает, что любой модуль, линкующийся с AppCore, автоматически получает доступ к Qt Core, что разумно, поскольку публичные заголовки AppCore сами используют типы из Qt Core. AppNetwork зависит от Qt6::Network только PRIVATE — потому что использование сети — деталь реализации, не «протекающая» в публичный интерфейс AppNetwork (если, конечно, публичные заголовки модуля не возвращают типы из QtNetwork напрямую).
AppUi демонстрирует ключевую идею: AppNetwork указан как PRIVATE, то есть MyApp, линкуясь с AppUi, не получает прямого доступа к AppNetwork и не «видит» его заголовки — это инкапсулирует сетевой слой внутри UI-модуля, если внешний код не должен напрямую обращаться к API сети.
Подводные камни
- Объявление всех зависимостей как PUBLIC «на всякий случай» превращает граф зависимостей в плоскую кашу, где любое изменение в глубине дерева требует пересборки всего проекта — это убивает главное преимущество модульной архитектуры.
- Циклические зависимости между модулями (
AppCoreзависит отAppUi, аAppUiотAppCore) не поддерживаются статической линковкой CMake и потребуют рефакторинга — выделения общего кода в отдельный, более низкоуровневый модуль. - Несогласованность
target_include_directories. Если заголовки указаны какPRIVATE include, а должны быть видны зависимым модулям, при сборке зависимого таргета компилятор не найдёт нужные заголовки — частая причина непонятных ошибокfile not foundпри рефакторинге структуры проекта. - Смешивание старого (directory-based,
include_directories(),link_directories()) и нового (target-based) стиля CMake в одном проекте усложняет понимание реальных зависимостей — современный CMake рекомендует полностью отказаться от директивных команд в пользуtarget_*.