Lesson 9 · Targets: the core model

Headers and include directories

How the compiler finds your headers, and what PRIVATE, PUBLIC and INTERFACE mean in plain words, with a real example.

The most common “why does not my build work” in CMake is include directories. This lesson makes them mechanical.

The problem

Your library has two kinds of headers:

calc/
├── include/calc/calc.h      # for the outside world
└── src/internal.h           # for calc's own code

The compiler needs to find both. But the outside world should only ever see include/. One command handles both cases:

target_include_directories(calc PUBLIC  include)
target_include_directories(calc PRIVATE src)

Two lines, and each folder goes exactly where it belongs. Now the keywords.

What PRIVATE, PUBLIC and INTERFACE mean

First, one word you need to know. The consumer is any other target that uses your library. In the examples below, the consumer is the executable app, which links calc. When we say “consumers get it”, we mean “any target that links calc gets it”.

Now the three keywords, in plain words:

  • PRIVATE. Only this target’s own files need it. PRIVATE src means “when compiling calc.cpp, look in src/”. Nobody else ever sees that folder.

  • PUBLIC. This target needs it, and consumers need it too. PUBLIC include means “when compiling calc.cpp, look in include/. And when app links calc, add include/ to app as well”. That is why app can write #include <calc/calc.h>.

  • INTERFACE. Only consumers need it. The target itself never uses it. This happens with header-only libraries, which have no .cpp files to compile. Lesson 13 covers them.

A rule of thumb. If calc.h uses it, it is PUBLIC. If only calc.cpp uses it, it is PRIVATE. If only people using calc need it, it is INTERFACE.

What not to do

Don't do this
# OLD. Global include dirs. include_directories(include) # leaks into every target, including third-party include_directories(src) # also exposes private headers to everyone
Do this
# Attach the folders to the target that owns them. target_include_directories(calc PUBLIC include) target_include_directories(calc PRIVATE src)

Why

With include_directories, an add_subdirectory(third_party) after that line silently gives their targets your folders. If two libraries both have a version.h, the first one wins and you get the wrong file with no error. When the folder is attached to the target, “which version.h am I including” always has an answer.

Rules that save you pain

  1. Public headers. PUBLIC include/. One line, consumers inherit.
  2. Private headers. PRIVATE src/. Internal, invisible.
  3. Header-only libraries. INTERFACE include/. Nothing to compile, consumers only (lesson 13).
  4. In consumer code, write includes with the folder prefix, #include <calc/calc.h>. Never a bare #include "calc.h" with a flat include folder.

Tip

If a consumer still cannot find a header, look at the actual compiler flags. Run cmake --build build --target app -v and check the -I entries (lesson 27). Nine times out of ten, a missing PUBLIC is the culprit.

Note

The full comparison of the three scopes, with examples, is on the PUBLIC vs PRIVATE vs INTERFACE page.

Read the docs