Lesson 13 · Targets: the core model

Interface libraries

add_library(... INTERFACE), a target with no sources. The clean way to bundle headers, flags and requirements and hand them to consumers.

A target without any source files sounds useless. It is actually one of the most useful things in CMake. Interface libraries exist purely to carry requirements to other targets.

The header-only library

The simplest use is a library that is just headers. There is nothing to compile, so it is an INTERFACE library:

mystr/
├── CMakeLists.txt
└── include/mystr/
    └── mystr.hpp
CMakeLists.txt
cmake_minimum_required(VERSION 4.4) project(mystr LANGUAGES CXX) # INTERFACE. No sources, only requirements for consumers. add_library(mystr INTERFACE) target_include_directories(mystr INTERFACE include) target_compile_features(mystr INTERFACE cxx_std_20)

Consumers get everything with one line:

target_link_libraries(app PRIVATE mystr)   # include dir + C++20 requirement, done

The “warnings” bundle

The same idea for flags. One target that is a set of warnings:

CMakeLists.txt
# A target named "warnings". It compiles nothing, it carries flags. add_library(warnings INTERFACE) if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "MSVC") target_compile_options(warnings INTERFACE /W4) else() target_compile_options(warnings INTERFACE -Wall -Wextra -Wpedantic) endif()
target_link_libraries(app      PRIVATE warnings)
target_link_libraries(mylib    PRIVATE warnings)
target_link_libraries(test_app PRIVATE warnings)

One definition of “warnings”, applied everywhere. Changing it once changes the whole project.

Why

Interface libraries turn repeated boilerplate into a name. Instead of copying an if(MSVC) block into ten targets, you define it once and link it ten times. You can also compose them. add_library(best_practices INTERFACE) that links warnings and sets the C++ standard, then target_link_libraries(app PRIVATE best_practices) gives the app everything.

INTERFACE means “consumers only”

            compiles itself?   consumers inherit?
PRIVATE              ✓                 ✗
PUBLIC               ✓                 ✓
INTERFACE            ✗                 ✓

An INTERFACE target has nothing to compile, so everything on it must be INTERFACE. Include dirs, features, options, links. Since 3.19 an INTERFACE library can carry sources with target_sources(... INTERFACE), but that is a niche “headers as sources for IDEs” trick. Ignore it until you need it.

ALIAS, a stable name

When you rename a library, consumers should not care. An alias gives a target a second, stable name:

add_library(calc STATIC src/calc.cpp)
add_library(calc::calc ALIAS calc)     # consumers link calc::calc

The name::name pattern is the convention used by imported targets (lesson 14). Aliases let your project follow the same convention. Note that an alias to an INTERFACE library works too, but aliases cannot be installed or exported. The export story uses a different mechanism (lesson 22).

Pitfall

Do not use an INTERFACE library where a real one is needed. If it has .cpp files, make it STATIC or SHARED. An INTERFACE library with PRIVATE compile flags is a logic error. There is nothing to compile, so the flags vanish.

Note

The full comparison of PRIVATE, PUBLIC and INTERFACE is on the PUBLIC vs PRIVATE vs INTERFACE page.

Read the docs