Cmake

Видимость зависимостей: PUBLIC/PRIVATE/INTERFACE в target_link_libraries

Введение

Статья 574 уже использовала PRIVATE в target_link_libraries без подробного объяснения — в этой статье разберём три ключевых спецификатора видимости (PUBLIC, PRIVATE, INTERFACE), определяющих, как именно настройки и зависимости одной цели распространяются (или не распространяются) на цели, которые от неё в свою очередь зависят — одна из самых важных, но часто недопонимаемых концепций современного CMake.

Концепция

PRIVATE зависимость используется текущей целью для своей собственной реализации, но НЕ распространяется на цели, зависящие от текущей цели — если библиотека A приватно зависит от библиотеки B, то цель C, зависящая от A, не получает автоматического доступа к B и её настройкам через эту зависимость. PUBLIC зависимость используется и текущей целью, и автоматически распространяется на все цели, зависящие от текущей — это нужно, когда зависимость «видна» в публичном интерфейсе текущей библиотеки (например, тип из библиотеки B используется как параметр публичной функции библиотеки A, и потребители A, соответственно, тоже должны иметь доступ к заголовкам B). INTERFACE зависимость, в противоположность PRIVATE, НЕ используется текущей целью для собственной реализации, но распространяется на зависящие от неё цели — менее частый случай, обычно применимый для INTERFACE-библиотек (статья 574) или специфичных ситуаций, где сама текущая цель не нуждается в зависимости непосредственно, но обязана передать её дальше.

Пример кода: Видимость зависимостей

# Демонстрация разницы PRIVATE/PUBLIC через реалистичный пример трёх связанных библиотек
add_library(JsonParser json_parser.cpp) # внутренняя реализация использует какую-то JSON-библиотеку
add_library(SomeJsonLibrary INTERFACE) # представим, что это внешняя, header-only JSON-библиотека

target_link_libraries(JsonParser PRIVATE SomeJsonLibrary)
# ^ PRIVATE — JsonParser использует JSON-библиотеку ВНУТРИ своей реализации,
#   но не "протекает" наружу — потребители JsonParser НЕ обязаны знать о SomeJsonLibrary,
#   если публичный API JsonParser НЕ использует типы из SomeJsonLibrary напрямую

add_library(NetworkClient network_client.cpp network_client.h)
target_link_libraries(NetworkClient PUBLIC JsonParser)
# ^ PUBLIC — если network_client.h ВОЗВРАЩАЕТ или ПРИНИМАЕТ типы из JsonParser
#   в своих публичных функциях, потребители NetworkClient АВТОМАТИЧЕСКИ
#   получают доступ к заголовкам JsonParser, без необходимости отдельно его подключать

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE NetworkClient)
# MyApp автоматически получает доступ И к NetworkClient, И (благодаря PUBLIC выше) к JsonParser,
# но НЕ к SomeJsonLibrary (благодаря PRIVATE на уровне JsonParser) — если только сам не подключит её отдельно
# Практическая проверка — какие заголовки реально ДОСТУПНЫ для конкретной цели
# main.cpp может содержать:
#include "network_client.h" // OK — прямая, явная зависимость
#include "json_parser.h"    // OK — благодаря PUBLIC от NetworkClient на JsonParser
#include "some_json_lib.h"  // ОШИБКА КОМПИЛЯЦИИ — недоступен, поскольку JsonParser->SomeJsonLibrary это PRIVATE

Пояснения к коду

Цепочка MyApp → NetworkClient → JsonParser → SomeJsonLibrary с разными спецификаторами видимости на каждом шаге показывает, как именно «просачивание» зависимостей контролируется индивидуально для каждого звена этой цепочки — PUBLIC от NetworkClient к JsonParser означает, что заголовки JsonParser автоматически становятся доступны и для MyApp (через транзитивное распространение), тогда как PRIVATE от JsonParser к SomeJsonLibrary «обрывает» дальнейшее распространение именно на этом конкретном шаге, и MyApp не получает автоматического доступа к этой более глубокой, «внутренней» зависимости JsonParser. Закомментированный пример с попыткой подключить заголовок some_json_lib.h непосредственно из main.cpp показывает конкретное, наблюдаемое следствие этой настройки — компилятор не найдёт соответствующий путь включения, поскольку PRIVATE-зависимость JsonParser от SomeJsonLibrary намеренно не передаёт этот путь включения дальше, к MyApp.

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

  • Использование PUBLIC по умолчанию «на всякий случай», без реального анализа, действительно ли зависимость видна в публичном интерфейсе текущей цели — это самая частая, практически значимая ошибка при работе с видимостью зависимостей, и избыточное использование PUBLIC там, где была бы достаточна PRIVATE, «просачивает» внутренние, реализационные детали текущей цели наружу, ко всем её потребителям, что усложняет граф зависимостей всего проекта и может затруднить последующую замену внутренней реализации (например, замену одной JSON-библиотеки на другую) без затрагивания всех транзитивных потребителей, которые формально не должны были знать об этой внутренней детали реализации.
  • Использование PRIVATE для зависимости, типы которой реально используются в публичном заголовочном файле текущей цели — это прямо противоположная ошибка: если network_client.h реально объявляет публичную функцию, принимающую или возвращающую тип из JsonParser, но зависимость от JsonParser помечена как PRIVATE, потребители NetworkClient получат ошибку компиляции при попытке использовать эту публичную функцию (компилятор не найдёт определение используемого в её сигнатуре типа), что является прямым, наблюдаемым следствием неверно выбранной видимости.
  • Путаница между видимостью зависимости (PUBLIC/PRIVATE/INTERFACE в target_link_libraries) и видимостью отдельных свойств (PUBLIC/PRIVATE/INTERFACE в target_include_directories/target_compile_definitions, статья 576) — хотя используется тот же набор из трёх ключевых слов с аналогичной общей логикой «что видно текущей цели и что распространяется далее», это технически разные команды CMake, применяемые к разным аспектам конфигурации цели, и согласованное, продуманное использование всех соответствующих target_* команд с правильно выбранной видимостью для каждого конкретного свойства/зависимости — важная часть качественного, поддерживаемого описания сборки проекта.
  • Игнорирование транзитивности PUBLIC-зависимостей при анализе реального графа зависимостей проекта, особенно для крупных проектов с глубокими цепочками зависимостей между множеством целей — реальный, эффективный набор зависимостей конкретной цели может оказаться значительно шире явно перечисленных непосредственно для неё зависимостей, именно из-за транзитивного распространения через PUBLIC на промежуточных уровнях, и понимание полной картины графа зависимостей (а не только непосредственных, прямых связей) важно для осознанного управления сложностью зависимостей крупного проекта.