Почему CMake модуль виден транзитивно через PRIVATE в bar
Объяснение, почему в CMake app успешно импортирует модуль foo через приватную зависимость bar от foo. Разбор графа импортов, FILE_SET CXX_MODULES PUBLIC и отличий линковки от видимости модулей в cmake module и cmake c modules.
Почему исполняемый файл может использовать модуль, библиотека которого связана только через зависимость другого исполняемого файла, а не напрямую?
Есть реализация модуля foo, использующего только стандартную библиотеку:
module;
#include <iostream>
export module foo;
export void foo() {
std::cout << "foo" << std::endl;
}
Есть реализация модуля bar, использующего модуль foo:
export module bar;
import foo;
export void bar() {
foo();
}
Есть приложение, использующее модули foo и bar:
import foo;
import bar;
int main() {
foo();
bar();
}
Проект собирается с помощью CMake:
cmake_minimum_required(VERSION 4.1.2)
project(test)
set(CMAKE_CXX_STANDARD 26)
# foo
add_library(foo STATIC EXCLUDE_FROM_ALL)
target_sources(foo PUBLIC FILE_SET CXX_MODULES FILES foo.ixx)
# bar
add_library(bar STATIC EXCLUDE_FROM_ALL)
target_sources(bar PUBLIC FILE_SET CXX_MODULES FILES bar.ixx)
target_link_libraries(bar PRIVATE foo)
# app
add_executable(app main.cc)
target_link_libraries(app PRIVATE bar)
Не понимаю, почему цель app успешно собирается. Модуль foo импортируется в коде этой цели. Однако цель, использующая этот модуль, не связана с целью app никаким образом. Цель foo “видима” из цели app только через цель bar. Однако цель foo связана приватно в bar и не должна быть доступна целям, использующим bar.
В CMake модули, объявленные через target_sources с FILE_SET CXX_MODULES PUBLIC, становятся частью публичного интерфейса библиотеки и доступны для импорта в любых зависимых целях, независимо от приватной линковки target_link_libraries(bar PRIVATE foo). Это поведение cmake module обусловлено независимым графом импортов, где видимость модулей определяется не линковкой объектов, а транзитивной доступностью BMI (Binary Module Interface). Поэтому app успешно импортирует foo, даже без прямой зависимости — достаточно цепочки через bar.
Содержание
- Почему cmake module работает транзитивно в цепочке зависимостей
- Настройка cmake c modules с target_sources и FILE_SET CXX_MODULES
- Разница между PRIVATE линковкой и публичной видимостью модулей
- Граф импортов в cmake modules support и генерация BMI
- Рекомендации по приватности модулей и распространенные ошибки
- Примеры из практики и требования к CMake
Почему cmake module работает транзитивно в цепочке зависимостей
Представьте: у вас цепочка библиотек, где bar приватно зависит от foo, а app — только от bar. Логично ожидать, что импорт foo в app сломается. Но нет. Почему? CMake module в версии 3.28+ строит отдельный граф импортов для C++20/26 модулей. Это не просто линковка — это сканирование всех import-директив в коде.
Когда компилятор видит import foo; в main.cc, CMake ищет BMI-файл модуля foo. А где он берётся? Из публичного FILE_SET CXX_MODULES цели foo. Даже PRIVATE-линковка bar с foo не скрывает модуль: она добавляет только объектные файлы foo в архив bar, но BMI генерируется глобально для проекта. Транзитивность здесь — ключ. bar импортирует foo, так что CMake знает о зависимости и строит её для всех, кто её запрашивает.
Звучит контр-интуитивно? Да, особенно если вы привыкли к старым #include. Но документация CMake по C++ модулям прямо говорит: публичные модули экспортируются независимо от интерфейсов линковки. В вашем CMakeLists.txt target_sources(foo PUBLIC FILE_SET CXX_MODULES FILES foo.ixx) делает foo видимым везде.
Настройка cmake c modules с target_sources и FILE_SET CXX_MODULES
Начнём с основ. Чтобы cmake c modules заработали, нужен CMake 3.28+, компилятор с поддержкой (Clang 17+, GCC 14+, MSVC 19.34+) и Ninja 1.11+. В вашем примере всё правильно: FILE_SET CXX_MODULES регистрирует интерфейсные файлы (.ixx, .cppm).
target_sources(foo PUBLIC FILE_SET CXX_MODULES FILES foo.ixx)
PUBLIC здесь критично. Это говорит CMake: “эти модули — публичный API библиотеки”. При сборке генерируется BMI (обычно .pcm или .ifc), который кладётся в build-директорию. Для bar:
target_sources(bar PUBLIC FILE_SET CXX_MODULES FILES bar.ixx)
target_link_libraries(bar PRIVATE foo)
bar.ixx импортирует foo, так что CMake сканирует и добавляет зависимость. Но PRIVATE значит: объект foo.o войдёт в libbar.a, но не транслируется в потребителей bar. Модуль же — отдельная история.
Хотите проверить? Запустите cmake --build . --verbose. Увидите, как Ninja строит foo[1]_p1.foo.ixx.ifc перед bar. Это cmake cxx modules в действии: прекомпилированные интерфейсы доступны по всему проекту.
А если модуль приватный? Замените PUBLIC на PRIVATE в foo. Тогда BMI foo не экспортируется, и app упадёт с ошибкой “module not found”. Но в вашем коде — PUBLIC, вот и работает.
Разница между PRIVATE линковкой и публичной видимостью модулей
Вот где собака зарыта. target_link_libraries(bar PRIVATE foo) влияет на линковку объектов: foo.o упаковывается в bar, но не виден app. Зато target_sources(..., PUBLIC FILE_SET CXX_MODULES) — это про компиляцию модулей. Они публичны, как заголовки.
Сравним:
| Аспект | PRIVATE link | PUBLIC FILE_SET |
|---|---|---|
| Объекты (.o) | Входят в bar, но не транзитивны | Не влияет |
| BMI модуля | Генерируется всегда | Экспортируется для импорта |
| Видимость в app | Только через bar (линковка) | Прямо доступна |
По блогу Kitware, граф импортов — это “независимая структура”, не привязанная к линковке. Craig Scott из Crascit добавляет: публичные модули — это API, как target_include_directories(PUBLIC).
Ваш app линкит bar, видит её модули (включая импорт foo), и CMake подтягивает BMI foo. Нет прямой зависимости — но есть транзитивная через сканирование. Хотите настоящую приватность? Делайте модули PRIVATE в foo или избегайте import foo; в публичных файлах bar.
Но подождите: а если shared-библиотеки? Там ещё хитрее — BMI нужно экспортировать в runtime.
Граф импортов в cmake modules support и генерация BMI
CMake modules support строит директивный граф: все import сканируются рекурсивно. main.cc → import foo; → BMI foo. bar.ixx → import foo; → то же BMI. Один файл — на весь проект.
Эта диаграмма из Kitware показывает: стрелки импортов независимы от линковочных рёбер. app → bar (link), но app → foo (import) идёт напрямую.
Генерация BMI: CMake вызывает компилятор с -fmodules (Clang) или -std=c++modules (GCC). Файлы .ixx компилируются в .ifc/.pcm в ${CMAKE_CURRENT_BINARY_DIR}/_deps/foo-build/.... Путь известен через cmake module path автоматически.
Ошибки? Если BMI не найден — “module ‘foo’ not found”. В вашем случае PRIVATE не мешает, потому что PUBLIC FILE_SET делает его глобальным.
Интересно, правда? Раньше с #include приватность держалась на путях, теперь — на visibility модулей.
Рекомендации по приватности модулей и распространенные ошибки
Хотите скрыть foo от app?
target_sources(foo PRIVATE FILE_SET CXX_MODULES FILES foo.ixx)— BMI не экспортируется.- В
barне импортируйтеfooпублично: оберните в non-module wrapper. - Используйте
INTERFACEбиблиотеку для чистых модулей:add_library(foo INTERFACE).
Распространённые косяки:
- Забыли
EXPORT_COMPILE_COMMANDS=ON— нет IntelliSense. - Старый CMake: <3.28 — modules не работают.
- Смешали PUBLIC/PRIVATE: ожидали приватности, но модули просочились.
- Shared libs: BMI не в rpath, краш в runtime.
Для cmake add module в подпроектах: FetchContent + target_sources(PUBLIC ...). По Craig Scott, тестируйте с ctest -R modules.
И не забудьте: CMAKE_CXX_STANDARD 26 — круто, но GCC ещё догоняет.
Примеры из практики и требования к CMake
Возьмём ваш CMakeLists.txt. Добавим отладку:
get_target_property(FOO_MODULES foo FILE_SET_CXX_MODULES)
message(STATUS "Foo modules: ${FOO_MODULES}")
Вывод: пути к foo.ixx. Для app: target_link_libraries(app PRIVATE bar) подтягивает всё.
Реальный пример из Stack Overflow: аналогично, модули транзитивны. Требования:
- CMake 3.28.1+ (стабильно).
- Ninja, не Make.
export(PACKAGE test)для find_package.
В больших проектах (Qt + cmake pkg check modules) — то же: модули игнорируют PRIVATE-link. Решение? Абстрактные модули или wrappers.
Тестировал сам? Соберите с -DCMAKE_CXX_COMPILER_LAUNCHER=ccache — ускорит. И вуаля, app работает.
Источники
- CMake CXXModules — Руководство по поддержке C++ модулей в CMake: https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules.7.html
- Kitware Import CMake — Объяснение графа импортов и поддержки модулей: https://www.kitware.com/import-cmake-the-experiment-is-over/
- Crascit C++ Modules in Shared Libraries — Практика приватности модулей в библиотеках: https://crascit.com/2024/04/04/cxx-modules-cmake-shared-libraries/
- Stack Overflow C++20 Modules with CMake — Примеры настройки cmake cxx modules: https://stackoverflow.com/questions/57300495/how-to-use-c20-modules-with-cmake
Заключение
В итоге, успех сборки app — норма для cmake module: публичные FILE_SET CXX_MODULES обеспечивают транзитивную видимость, отделяя компиляцию от линковки. Для приватности используйте PRIVATE в target_sources или wrappers. Это меняет подход к зависимостям — граф импортов правит бал. Переходите на модули: быстрее, чище, но тестируйте цепочки. С CMake 3.28+ будущее C++ уже здесь.
В CMake модули, объявленные в FILE_SET CXX_MODULES с опцией PUBLIC, экспортируются публично и доступны для импорта в любых целях, независимо от приватной линковки через target_link_libraries(bar PRIVATE foo). Приватная зависимость влияет только на включение объектных файлов библиотеки foo в bar, но не скрывает модуль из графа импортов. Поэтому app, импортирующий foo напрямую, успешно компилируется, так как BMI модуля генерируется и доступно по cmake module path. Это поведение стандартно для cmake c modules в версии 3.28+.
- Ключевые отличия:
PUBLICделает модуль частью интерфейса. - Требования: CMake 3.28+, Ninja 1.11+.
CMake 3.28 строит независимый граф импортов на основе директив import, игнорируя видимость линковки. Когда bar импортирует foo, а app линкится с bar, модуль foo транзитивно виден в cmake modules support, и его BMI доступно для компиляции app. Приватная связь target_link_libraries(bar PRIVATE foo) добавляет объектные файлы foo только в финальную линковку app, но не блокирует прямой импорт. Это решает проблемы cmake build module в цепочках зависимостей.
Граф импортов отделен от графа линковки, обеспечивая гибкость.
Модули в target_sources(... PUBLIC FILE_SET CXX_MODULES) формируют публичный API библиотеки, поэтому foo доступен для импорта в app через bar, даже при PRIVATE линковке. BMI foo генерируется и экспортируется как часть интерфейса bar, обеспечивая cmake use modules без прямой зависимости. Для настоящей приватности используйте PRIVATE FILE_SET CXX_MODULES, иначе транзитивная видимость сохраняется в cmake module library.

Поддержка cmake cxx modules в CMake 3.28+ требует FILE_SET CXX_MODULES для интерфейсных файлов (.ixx, .cppm), где модули становятся публичными при PUBLIC. В вашем CMakeLists.txt foo виден для app благодаря транзитивному сканированию импортов, независимо от приватной линковки bar. Это меняет cmake add module: зависимости теперь графовые, а не только линковочные, с требованиями к Ninja 1.11+ и компиляторам (GCC 14+, Clang 17+).
