Getting these keywords right is what makes libraries easy to consume and builds correct.
Modern CMake attaches every setting to a target with target_* commands, instead of setting global variables:
target_include_directories(core PUBLIC include PRIVATE src)target_compile_definitions(core PRIVATE CORE_INTERNAL=1)target_compile_options(core PRIVATE -Wall)target_compile_features(core PUBLIC cxx_std_20)target_link_libraries(core PUBLIC fmt::fmt PRIVATE zlib)
The keyword decides who gets the setting:
PRIVATE: only for building this target. Implementation details:src/headers, warnings, libraries used only in.cppfiles.INTERFACE: only for targets that link to this one, not for itself. Typical for header-only libraries.PUBLIC: both. Use it when the setting appears in your public headers: the public include directory, a dependency whose types appear in your API, a required C++ standard.
The rule of thumb: if a user of your library needs it to compile against your headers, it's PUBLIC; otherwise PRIVATE. Propagation is transitive: if app links core and core links fmt PUBLIC, app gets fmt's include path too.
Avoid the old directory-wide commands include_directories(), add_definitions() and link_libraries(); they apply to every target in the directory and hide which target needs what.
Example
# header-only utility: nothing to compile, only requirements for users
add_library(mathx INTERFACE)
target_include_directories(mathx INTERFACE mathx/include)
target_compile_features(mathx INTERFACE cxx_std_20)
add_library(core STATIC src/engine.cpp)
target_include_directories(core
PUBLIC include # users include <core/engine.h>
PRIVATE src) # internal headers
target_compile_definitions(core PRIVATE CORE_BUILDING=1)
target_link_libraries(core
PUBLIC mathx # mathx types appear in core's public headers
PRIVATE Threads::Threads)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)
# app gets: include/, mathx/include, C++20
# app does not get: src/, CORE_BUILDING, (Threads only at link time for static core)Remember
- PRIVATE: only this target; INTERFACE: only users; PUBLIC: both
- If it's needed to compile against your headers, make it PUBLIC
- Use target_* commands, not directory-wide include_directories
Watch out for
- Making everything PUBLIC, which leaks internal include paths and flags into every dependent target.