CMakeMiddleTechnical

В чём разница между PRIVATE, PUBLIC и INTERFACE в свойствах таргетов CMake?

PRIVATE — свойство только для самого таргета (реализация), PUBLIC — для таргета и всех его потребителей (публичный API), INTERFACE — только для потребителей (header-only библиотеки). Правильный выбор минимизирует транзитивные зависимости.

PRIVATE, PUBLIC и INTERFACE в CMake

Когда вы вызываете target_link_libraries(), target_include_directories(), target_compile_definitions() или target_compile_options(), вы должны указать одно из трёх ключевых слов: PRIVATE, PUBLIC или INTERFACE. Они определяют, кто видит добавляемое свойство таргета.

Суть трёх ключевых слов

  • PRIVATE — свойство используется только самим таргетом при сборке. Потребители таргета его не наследуют.
  • PUBLIC — свойство используется и самим таргетом, и передаётся всем потребителям (транзитивно).
  • INTERFACE — свойство НЕ используется самим таргетом, но передаётся всем потребителям. Типично для header-only библиотек.

Наглядный пример

cmake_minimum_required(VERSION 3.21)
project(Example)

find_package(fmt REQUIRED)
find_package(Boost REQUIRED COMPONENTS filesystem)

add_library(mylib STATIC mylib.cpp)

# fmt нужен только для реализации mylib.cpp — не нужен потребителям
target_link_libraries(mylib PRIVATE fmt::fmt)

# Boost filesystem нужен и реализации, и потребителям (API возвращает boost::filesystem::path)
target_link_libraries(mylib PUBLIC Boost::filesystem)

# Заголовки src/internal/ нужны только при компиляции mylib.cpp
target_include_directories(mylib PRIVATE src/internal)

# Заголовки include/ нужны и нам, и всем потребителям
target_include_directories(mylib PUBLIC include/)

add_executable(myapp main.cpp)
# myapp автоматически получает:
# - include/ от mylib (PUBLIC)
# - Boost::filesystem от mylib (PUBLIC)
# НО НЕ получает:
# - fmt::fmt (PRIVATE)
# - src/internal (PRIVATE)
target_link_libraries(myapp PRIVATE mylib)

INTERFACE: header-only библиотека

# Header-only библиотека не компилируется сама
add_library(myheaders INTERFACE)

# INTERFACE: не нужно компилировать, но нужно потребителям
target_include_directories(myheaders INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>
)
target_compile_features(myheaders INTERFACE cxx_std_17)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE myheaders)
# myapp получит include-путь и флаг c++17 автоматически

Транзитивность зависимостей

# libA зависит от libBase
add_library(libBase STATIC base.cpp)
target_include_directories(libBase PUBLIC base/include)

add_library(libA STATIC a.cpp)
target_link_libraries(libA PUBLIC libBase)   # транзитивно передаёт libBase

add_executable(app main.cpp)
target_link_libraries(app PRIVATE libA)
# app получает: libA + libBase (из PUBLIC цепочки) + base/include

Правило выбора

  • Зависимость нужна только реализации (.cpp), не видна в публичном API (.h) → PRIVATE
  • Зависимость видна в публичном API (в заголовках библиотеки) → PUBLIC
  • Библиотека header-only или зависимость нужна только при использовании, но не при сборке → INTERFACE

Подводные камни

  • Всё PUBLIC «на всякий случай»: создаёт избыточный граф зависимостей, замедляет инкрементальную сборку, загрязняет пространство имён потребителей.
  • PRIVATE для зависимостей из публичного API: если заголовок библиотеки включает заголовок зависимости, а зависимость объявлена PRIVATE — потребитель не сможет скомпилировать код, использующий библиотеку.
  • INTERFACE для статической библиотеки: у STATIC библиотек сам таргет компилируется, поэтому INTERFACE без PRIVATE обычно некорректно.
  • Транзитивные дубликаты: длинные PUBLIC-цепочки дублируют флаги в командной строке линковщика, что в редких случаях вызывает проблемы с длиной командной строки (Windows).
  • INTERFACE и generator expressions: $ и $ нужны при экспорте таргетов — без них include-пути после install() будут указывать на build-директорию, а не install-директорию.

Common mistakes

  • Объяснять PRIVATE, PUBLIC и INTERFACE только по синтаксису, без жизненного цикла и стоимости.
  • Игнорировать ошибки, null/empty состояния, порядок инициализации или режим сборки.
  • Давать пример, который работает в демо, но ломается при изменении владельца ресурса.
  • Трактовать CMake как shell-скрипт вместо описания графа таргетов.

What the interviewer is testing

  • Кандидат формулирует точную модель для PRIVATE, PUBLIC и INTERFACE, а не только определение.
  • Пример компилируем, безопасен по lifetime и соответствует версии технологии.
  • Названы trade-off, ограничения и диагностируемые симптомы ошибки.
  • Разделяет configure/generate/build и использует target-based подход.

Sources

Related topics