CMake Targets in C++ | PUBLIC/PRIVATE, Propagation & Modern CMake

CMake Targets in C++ | PUBLIC/PRIVATE, Propagation & Modern CMake

이 글의 핵심

Target-based CMake: replace global include_directories with target_link_libraries and clear PUBLIC/PRIVATE/INTERFACE semantics.

What are CMake Targets? Why Target Based?

Problem Scenario: Confusion in global settings

Problem: Older CMake styles used global commands like include_directories() and link_libraries(). As a project grows, it becomes difficult to keep track of which target uses which header, and dependencies become complicated.

# ❌ Old school: global settings
include_directories(/usr/local/include)
link_libraries(boost_system)

add_executable(app1 main1.cpp)
add_executable(app2 main2.cpp)
# app1, app2 are both linked to boost_system (maybe unintentional)

Solution: Using target-based commands (target_*) makes the dependencies of each target clear and eliminates unnecessary links.

# ✅ Modern: Target-specific settings
add_executable(app1 main1.cpp)
target_include_directories(app1 PRIVATE /usr/local/include)
target_link_libraries(app1 PRIVATE Boost::system)

add_executable(app2 main2.cpp)
# app2 is not linked to boost

What is a target?

Target refers to the target (executable file, library) that CMake will build. Create targets with add_executable and add_library, and set the properties (header path, link library, compilation options) of each target with the target_* command.

flowchart TD
    subgraph targets["targets"]
        exe["add_executable(myapp)"]
        lib["add_library(mylib)"]
    end
    subgraph properties["Target properties"]
        inc["target_include_directories"]
        link["target_link_libraries"]
        opt["target_compile_options"]
        def["target_compile_definitions"]
    end
    exe --> inc
    exe --> link
    lib --> inc
    lib --> opt

index

  1. Create target: add_executable, add_library
  2. Target properties: target_* commands
  3. Visibility: PUBLIC, PRIVATE, INTERFACE
  4. Dependency propagation
  5. Frequently occurring problems and solutions
  6. Production Patterns
  7. Complete example: multi-library project

1. Create target

executable

# single source
add_executable(myapp main.cpp)

# Multiple sources
add_executable(myapp
    src/main.cpp
    src/utils.cpp
    src/config.cpp
)

# Use variables
set(APP_SOURCES
    src/main.cpp
    src/utils.cpp
)
add_executable(myapp ${APP_SOURCES})

Static library

add_library(mylib STATIC
    src/lib.cpp
    src/helper.cpp
)

Dynamic library

add_library(mylib SHARED
    src/lib.cpp
    src/helper.cpp
)

Header-only library

add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)

OBJECT library

# Create only object files (no linking)
add_library(myobj OBJECT
    src/common.cpp
)

# Reuse on multiple targets
add_executable(app1 main1.cpp $<TARGET_OBJECTS:myobj>)
add_executable(app2 main2.cpp $<TARGET_OBJECTS:myobj>)

2. Target properties: target_* commands

target_include_directories

add_library(mylib src/lib.cpp)

target_include_directories(mylib
PUBLIC include # exposed to the outside world
PRIVATE src/internal # Internal use only
)
add_executable(myapp main.cpp)

target_link_libraries(myapp PRIVATE
    mylib
    Boost::filesystem
    pthread
)

target_compile_options

target_compile_options(myapp PRIVATE
    -Wall
    -Wextra
    -Werror
    $<$<CONFIG:Debug>:-O0 -g>
    $<$<CONFIG:Release>:-O3>
)

target_compile_definitions

target_compile_definitions(myapp PRIVATE
    APP_VERSION="1.0"
    $<$<CONFIG:Debug>:DEBUG_MODE>
    $<$<PLATFORM_ID:Windows>:WINDOWS_BUILD>
)

target_compile_features

# C++20 feature requirements
target_compile_features(myapp PRIVATE cxx_std_20)

3. Visibility: PUBLIC, PRIVATE, INTERFACE

Concept

flowchart LR
    subgraph lib["mylib"]
        priv["PRIVATE\nInternal use only"]
        pub["PUBLIC\nExternal exposure"]
        iface["INTERFACE\nPropagation only"]
    end
    subgraph app["myapp"]
        use["use"]
    end
    pub --> use
    iface --> use
Keywordtarget yourselfDependent Target
PRIVATEuseDisabled
PUBLICuseuse
INTERFACEDisableduse

Practical example

# mylib: library
add_library(mylib src/lib.cpp)

target_include_directories(mylib
PUBLIC include # Targets linking mylib also use include/
PRIVATE src/internal # Only used inside mylib
)

target_compile_definitions(mylib
PUBLIC MYLIB_VERSION=1 # Target linking mylib is also defined
PRIVATE MYLIB_INTERNAL # defined only inside mylib
)

# myapp: executable file
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# myapp can include/ be used (PUBLIC)
# myapp cannot use src/internal (PRIVATE)
# MYLIB_VERSION defined in myapp (PUBLIC)

INTERFACE USE CASE

Used in header-only libraries.

add_library(header_only INTERFACE)

target_include_directories(header_only INTERFACE include)
target_compile_definitions(header_only INTERFACE HEADER_ONLY_LIB)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE header_only)
# myapp can use include/
# HEADER_ONLY_LIB defined in myapp

4. Dependency propagation

Transitive dependencies

# liba: lowest level library
add_library(liba STATIC a.cpp)
target_include_directories(liba PUBLIC include/a)

# libb: Depends on liba
add_library(libb STATIC b.cpp)
target_link_libraries(libb PUBLIC liba)
target_include_directories(libb PUBLIC include/b)

# myapp: Depends on libb
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE libb)
# myapp can use both include/a and include/b (PUBLIC propagation)
flowchart TD
    liba["liba\nPUBLIC include/a"]
    libb["libb\nPUBLIC include/b"]
    myapp["myapp"]
    
    libb -->|PUBLIC| liba
    myapp -->|PRIVATE| libb
    
    note1["myapp can use both include/a and include/b"]

Block transmission with PRIVATE

# libb links liba as PRIVATE
target_link_libraries(libb PRIVATE liba)

# myapp
target_link_libraries(myapp PRIVATE libb)
# myapp can only use include/b (liba is not propagated)

5. Frequently occurring problems and solutions

Issue 1: Using global commands

Cause: Global commands such as include_directories() and link_libraries() affect all future targets.

# ❌ Incorrect use
include_directories(/usr/local/include)
add_executable(app1 main1.cpp)
add_executable(app2 main2.cpp)
# Both app1 and app2 use /usr/local/include

# ✅ Correct use: Target-specific settings
add_executable(app1 main1.cpp)
target_include_directories(app1 PRIVATE /usr/local/include)

add_executable(app2 main2.cpp)
# app2 is not affected

Issue 2: PUBLIC/PRIVATE confusion

Symptom: Header not found, or unnecessary header exposed.

# ❌ Misuse: Internal header as PUBLIC
add_library(mylib src/lib.cpp)
target_include_directories(mylib PUBLIC src/internal)
# src/internal is exposed to the outside world

# ✅ Correct use
target_include_directories(mylib
PUBLIC include #API header
PRIVATE src/internal # implementation header
)

Problem 3: Circular dependencies

Symptom: CMake Error: Circular dependency.

Cause: A links B, and B links A.

# ❌ Incorrect use
add_library(liba a.cpp)
add_library(libb b.cpp)
target_link_libraries(liba PRIVATE libb)
target_link_libraries(libb PRIVATE liba) # Circulation!

# ✅ Correct use: Dependency redesign
# Common code that both liba and libb depend on is separated into libcommon
add_library(libcommon common.cpp)
add_library(liba a.cpp)
add_library(libb b.cpp)
target_link_libraries(liba PRIVATE libcommon)
target_link_libraries(libb PRIVATE libcommon)

Issue 4: OBJECT library linking

Cause: OBJECT libraries cannot be linked directly with target_link_libraries (prior to CMake 3.12).

# CMake 3.12+: OBJECT library linking possible
add_library(myobj OBJECT common.cpp)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE myobj)

# CMake 3.11 and below: Use $<TARGET_OBJECTS:>
add_executable(myapp main.cpp $<TARGET_OBJECTS:myobj>)

6. production pattern

Pattern 1: Common settings with interface libraries

# Project global compilation options to interface library
add_library(project_options INTERFACE)

target_compile_features(project_options INTERFACE cxx_std_20)

target_compile_options(project_options INTERFACE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra -Wpedantic>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

# Apply to all targets
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE project_options)

add_library(mylib lib.cpp)
target_link_libraries(mylib PRIVATE project_options)

Pattern 2: Alias ​​Target

add_library(mylib src/lib.cpp)
add_library(MyProject::mylib ALIAS mylib)

# Reference to the namespace elsewhere
target_link_libraries(myapp PRIVATE MyProject::mylib)

Pattern 3: Conditional Target

option(BUILD_TOOLS "Build command-line tools" ON)

if(BUILD_TOOLS)
    add_executable(tool1 tools/tool1.cpp)
    target_link_libraries(tool1 PRIVATE mylib)
endif()

Pattern 4: Target attribute lookup

# Get target properties
get_target_property(MYLIB_INCLUDES mylib INCLUDE_DIRECTORIES)
message(STATUS "mylib includes: ${MYLIB_INCLUDES}")

# Set target properties
set_target_properties(mylib PROPERTIES
    VERSION 1.0.0
    SOVERSION 1
    OUTPUT_NAME "my_library"
)

7. Complete example: multi-library project

Project structure

project/
├── CMakeLists.txt
├── core/
│   ├── CMakeLists.txt
│   ├── core.cpp
│   └── core.h
├── utils/
│   ├── CMakeLists.txt
│   ├── utils.cpp
│   └── utils.h
├── app/
│   ├── CMakeLists.txt
│   └── main.cpp
└── include/
    ├── core/
    │   └── core.h
    └── utils/
        └── utils.h

root CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MultiLib VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Common settings
add_library(project_options INTERFACE)
target_compile_features(project_options INTERFACE cxx_std_20)

add_subdirectory(core)
add_subdirectory(utils)
add_subdirectory(app)

core/CMakeLists.txt

add_library(core STATIC
    core.cpp
    core.h
)

target_include_directories(core
    PUBLIC ${CMAKE_SOURCE_DIR}/include/core
    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)

target_link_libraries(core PRIVATE project_options)

add_library(MultiLib::core ALIAS core)

utils/CMakeLists.txt

add_library(utils STATIC
    utils.cpp
    utils.h
)

target_include_directories(utils
    PUBLIC ${CMAKE_SOURCE_DIR}/include/utils
)

# Utils depend on core
target_link_libraries(utils
    PUBLIC MultiLib::core
    PRIVATE project_options
)

add_library(MultiLib::utils ALIAS utils)

app/CMakeLists.txt

add_executable(myapp main.cpp)

# If you link utils, core will also be linked automatically (PUBLIC propagation)
target_link_libraries(myapp PRIVATE
    MultiLib::utils
    project_options
)

Target command summary

commandDescription
add_executable(name sources...)Create executable target
add_library(name STATIC sources...)Create static library
add_library(name SHARED sources...)Create dynamic library
add_library(name INTERFACE)Header-only library
add_library(name OBJECT sources...)Create only object files
target_include_directories(target vis dirs...)Add header path
target_link_libraries(target vis libs...)Library Link
target_compile_options(target vis opts...)Add compilation options
target_compile_definitions(target vis defs...)Add preprocessing definition
target_compile_features(target vis features...)C++ Feature Requirements

organize

conceptDescription
TargetBuild target (executable, library)
target_*Setting properties for each target
PUBLICtarget + dependent target
PRIVATETarget only
INTERFACEDependent targets only (header only)
transitive dependencyAutomatic propagation to PUBLIC

CMake’s target-based approach clarifies dependencies and isolates build settings by target, making it easy to maintain even large projects.


FAQ

Q1: Global commands vs target commands?

A: Use target command (target_*). Global commands (include_directories, link_libraries) affect all targets, thereby tying up dependencies.

Q2: When should I use PUBLIC vs PRIVATE?

A: The header to be exposed externally is PUBLIC, and the internal implementation header is PRIVATE. When creating a library, set the API header to PUBLIC and the implementation header to PRIVATE.

Q3: When do I use INTERFACE?

A: Used by header-only library. If there are no sources to compile and only headers are provided, create them with add_library(name INTERFACE) and specify the header path with target_include_directories(name INTERFACE ...).

Q4: When do I use the OBJECT library?

A: Used when the same source is reused in multiple targets. You can avoid duplicate compilation by creating only the object file and including it as $<TARGET_OBJECTS:myobj> in multiple executable files/libraries.

Q5: Why use alias targets?

A: By adding a namespace with add_library(MyProject::mylib ALIAS mylib), you can reference external packages and internal targets in a consistent way. Like target_link_libraries(myapp PRIVATE MyProject::mylib Boost::filesystem), they are all unified in :: format.

Q6: What are the CMake Targets learning resources?

A:

One-line summary: CMake target-based approach allows for clear dependency management. Next, it would be a good idea to read CMake find_package.


Good article to read together (internal link)

Here’s another article related to this topic.

  • C++ CMake Complete Guide | Cross-platform build·latest CMake 3.28+ features·presets·modules
  • C++ CMake find_package complete guide | External library integration
  • C++ Conan Complete Guide | Modern C++ package management

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intent of the code clear?
  • Are there enough test cases?
  • Is it documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, cmake, targets, library, build, dependency, etc.


  • C++ CMake find_package complete guide | External library integration
  • C++ CMake |
  • C++ CMake Complete Guide | Cross-platform build·latest CMake 3.28+ features·presets·modules
  • C++ Conan Complete Guide | Modern C++ package management
  • CMake error |
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3