This guide explains CMake's PUBLIC and PRIVATE visibility modifiers for include directories, as demonstrated in the example_public_private module.
example_public_private/
├── include/
│ └── example_public_private.hpp # Public API header
└── src/
├── private_example.hpp # Internal implementation header
├── private_example.cpp
└── example_public_private.cpp
Headers in include/ constitute the library's public API and are accessible to consuming targets:
// main.cpp (consumer code)
#include "example_public_private.hpp" // AccessibleHeaders in src/ are implementation details and not accessible to consuming targets:
// main.cpp (consumer code)
#include "private_example.hpp" // Compilation errortarget_include_directories(example_public_private
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # Public API headers
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src # Implementation headers
)- Encapsulation: Enforces separation between interface and implementation
- Dependency Management: Consumers access only required components
- Build Optimization: Modifications to PRIVATE headers do not trigger dependent target rebuilds
- API Clarity: Explicitly defines public interface boundaries
When linking against a library using target_link_libraries:
# In main/CMakeLists.txt
target_link_libraries(main_exec
PRIVATE example_public_private
)The compiler receives:
- PUBLIC include directories from
example_public_private - PRIVATE include directories are not propagated
// In src/example_public_private.cpp
#include "example_public_private.hpp" // Accessible (PUBLIC)
#include "private_example.hpp" // Accessible (PRIVATE - same target)// In src/main/main.cpp
#include "example_public_private.hpp" // Accessible (PUBLIC)
#include "private_example.hpp" // Not accessible - compilation errorCMake supports three visibility levels:
- Visible only to the defining target
- Not propagated to dependent targets
- Use case: Implementation details, internal headers
- Visible to the defining target and all dependent targets
- Fully propagated through dependency chain
- Use case: Public API headers, required dependencies
- Not visible to the defining target
- Visible only to dependent targets
- Use case: Header-only libraries, transitive dependencies
Target A (library) Target B (executable)
├─ PUBLIC includes → Visible to B
├─ PRIVATE includes → Not visible to B
└─ INTERFACE includes → Visible to B (not visible to A)
To demonstrate visibility enforcement, attempt to include a private header in src/main/main.cpp:
#include "private_example.hpp"Expected compilation error:
fatal error: private_example.hpp: No such file or directory
This confirms that PRIVATE headers are correctly isolated from consuming targets.
- Place public API in
include/with PUBLIC visibility - Place implementation details in
src/with PRIVATE visibility - Use PUBLIC for dependencies referenced in public headers
- Use PRIVATE for dependencies used exclusively in implementation files
- Marking all include directories as PUBLIC without justification
- Mixing private and public headers in the same directory
- Using PUBLIC for implementation-only dependencies
- Exposing internal implementation details through public headers
add_library(mylib
src/mylib.cpp
src/internal.cpp
)
target_include_directories(mylib
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
)add_library(mylib INTERFACE)
target_include_directories(mylib
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include
)target_link_libraries(mylib
PUBLIC nlohmann_json # Required by public headers
PRIVATE sqlite3 # Used only in implementation
)cmake --build build --target help
cmake --build build --target mylib -- VERBOSE=1fatal error: some_header.hpp: No such file or directory
Potential causes:
- Header is PRIVATE but referenced from dependent target
- Missing
target_include_directoriesdirective - Incorrect visibility level specification
Standard library structure with install support:
mylib/
├── include/mylib/
│ ├── mylib.hpp
│ └── types.hpp
├── src/
│ ├── mylib.cpp
│ ├── internal.hpp
│ └── internal.cpp
└── CMakeLists.txt
CMakeLists.txt:
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)This configuration ensures:
- Consumers can include
<mylib/mylib.hpp> internal.hppremains inaccessible to consumers- Correct behavior in both build tree and post-installation