1. Include paths in CMake: target_include_directories
When you first encounter an error like fatal error: ...: No such file or directory, it's tempting to blame the computer: “The file is right here in the project!”. But the compiler is not a mind reader. It searches headers according to a set of directories (include paths), and if the required folder is not in that list, the file is considered “invisible”, even if it sits on your disk right next to your source.
It's important to distinguish two things. The #include directive in C++ says: “I need the text of this header at compile time.” Where exactly to look for that text is decided by compiler settings. Manually, via the command line, this is usually the -I... flag. In CMake we don't write -I directly (at least when using the target-based approach), but describe include paths via target_include_directories.
Imagine the compiler is a courier who received a note “pick up the package from the house calc.hpp”. If you didn't tell the courier which neighborhoods to search, they will follow their standard route and, of course, find nothing. They aren't obliged to scan your entire computer “just in case” — otherwise building any project would take forever.
What target_include_directories does and why it's a target property
In CMake include paths are set not “for the whole project”, but for a specific target: an executable or a library. This is the target-based approach: each target has its own properties, and include paths are one of the important properties because they directly affect compilation.
The minimal form of the command looks like this:
target_include_directories(my_target PRIVATE include)
Meaning: when compiling my_target the compiler will also search headers in the include/ folder (relative to the CMakeLists.txt where you wrote this). After that in C++ code you can write #include "math.hpp" (or #include "calc/add.hpp" — depending on the folder structure), and the compiler will find the file.
A very practical note: target_include_directories is usually placed after add_executable / add_library, because before creating the target there is nothing to configure.
add_executable(app src/main.cpp)
target_include_directories(app PRIVATE include)
This isn't “style”, but common sense: first we create the entity, then we set its properties.
Tutorial example: library calc and application app
To keep the topic from being abstract, let's continue with a single example. We'll make a simple project structure:
MiniProject/
CMakeLists.txt
include/
calc/
sum.hpp
src/
sum.cpp
main.cpp
The header declares a function, the .cpp defines it, and main.cpp uses it. The application will print the sum of two numbers (yes, it’s not “an Excel killer”, but it nicely exercises build discipline).
File include/calc/sum.hpp:
#pragma once
int sum(int a, int b);
File src/sum.cpp:
#include "calc/sum.hpp"
int sum(int a, int b) {
return a + b;
}
File src/main.cpp:
#include <iostream>
#include "calc/sum.hpp"
int main() {
std::cout << sum(2, 3) << '\n'; // 5
}
Now the question: how does the compiler know that the include/ folder exists at all and that it should look for calc/sum.hpp there? Answer: from target_include_directories.
Include path for the application: PRIVATE scenario
Let's start with a straightforward variant to feel the mechanism. Suppose for now we build everything in one target app (without a separate library), and we just want main.cpp to see headers from include/.
cmake_minimum_required(VERSION 3.20)
project(MiniProject LANGUAGES CXX)
add_executable(app
src/main.cpp
src/sum.cpp
)
target_compile_features(app PRIVATE cxx_std_23)
target_include_directories(app PRIVATE include)
Here PRIVATE is read as: “the include path is needed only to build this target app”.
Why does this make sense? Because app is the final executable. Usually it has no “consumers” that will link against it as a library. So there's no point in “passing” include paths further.
Note that src/sum.cpp also uses #include "calc/sum.hpp". That's fine: the include path applies to all .cpp files that are compiled as part of the target.
3. Visibility of include paths: PRIVATE / PUBLIC / INTERFACE
Why PUBLIC and INTERFACE exist at all
So far PRIVATE may look like “the only correct option”. But that's because we haven't made the main step yet: splitting into a library and an application.
When you have a library calc, you usually want two things.
First: when building the library itself the compiler must find its headers.
Second: when another application (our app) uses the library, it must also find the library headers — otherwise the consumer can't write #include "calc/sum.hpp".
And here the key word of the day appears: visibility.
What PUBLIC / PRIVATE / INTERFACE mean and how they “move” to the consumer
When you write:
target_include_directories(calc PUBLIC include)
you tell CMake two things at once:
1) the include path is needed by the library itself calc so it can compile (this is the “for itself” part).
2) the include path is needed by everyone who links calc so they can use the library's public headers (this is the “for consumers” part).
That's why PUBLIC is often described as “for itself and for others”.
To make it easier to remember, keep the following table in mind:
| Keyword | Needed when building the target itself? | Needed by consumers of the target? |
|---|---|---|
|
yes | no |
|
yes | yes |
|
no | yes |
The meaning of INTERFACE may at first seem odd: “how can an include path not be needed by the target itself?”. But it's very logical for special targets, for example header-only libraries (where there are no .cpp files) or interface-only targets that merely forward build settings.
Correctly building calc as a library: PUBLIC include
Let's rewrite CMake so that calc becomes a separate library and app becomes a separate executable that links against calc (we won't dive deeply into linking today, but the dependency itself is enough to demonstrate transitivity of include paths).
cmake_minimum_required(VERSION 3.20)
project(MiniProject LANGUAGES CXX)
add_library(calc
src/sum.cpp
)
target_compile_features(calc PRIVATE cxx_std_23)
target_include_directories(calc PUBLIC include)
add_executable(app
src/main.cpp
)
target_compile_features(app PRIVATE cxx_std_23)
target_link_libraries(app PRIVATE calc)
Note the nice idea: app does not have to call target_include_directories(app ...) for calc's headers. It receives the include path transitively because calc declared it as PUBLIC.
This is one of the main reasons to love the target-based approach: dependencies become “honest”. If you link a library, you automatically get everything needed to use it, but only what the library explicitly declared as a public requirement.
PRIVATE include path: implementation headers that consumers shouldn't see
Now let's make the project a bit more realistic. A library often has two kinds of headers:
- Public headers — what you allow library users to include. They usually live in include/.
- Internal implementation headers — what is needed only inside the library; consumers should not poke into them.
Let's make this structure:
include/
calc/
sum.hpp (public)
src/
detail/
sum_impl.hpp (internal)
sum.cpp
File src/detail/sum_impl.hpp:
#pragma once
inline int sum_impl(int a, int b) {
return a + b;
}
File src/sum.cpp:
#include "calc/sum.hpp"
#include "detail/sum_impl.hpp"
int sum(int a, int b) {
return sum_impl(a, b);
}
Now the calc library needs to be able to include headers from both include/ and src/ (because detail/ resides inside src/). But the consumer app definitely does not need to know about detail/sum_impl.hpp.
This is expressed in CMake like this:
target_include_directories(calc
PUBLIC include
PRIVATE src
)
Read this as: “the include/ folder is part of the public interface, while the src/ folder is the internal kitchen, not to be shown off”.
If someone in app now tries to do #include "detail/sum_impl.hpp", it will most likely fail to compile. And that's great: you've just put a fence around internal details.
INTERFACE include path: header-only library
The term INTERFACE usually becomes clear when you encounter a library without any .cpp. For example, you have a small set of functions in a header and you want to distribute only headers.
Let's make a mini-example: a library textutils, which will live in include/textutils/trim.hpp, and there will be no implementation .cpp.
include/
textutils/
trim.hpp
File include/textutils/trim.hpp:
#pragma once
#include <string>
inline std::string trim_one_space(std::string s) {
if (!s.empty() && s.front() == ' ') s.erase(s.begin());
if (!s.empty() && s.back() == ' ') s.pop_back();
return s;
}
In CMake such a library can be described as an interface target:
add_library(textutils INTERFACE)
target_include_directories(textutils INTERFACE include)
Here INTERFACE means: “the target itself does not compile anything, but forwards include paths and other requirements to consumers”.
And now app can “consume” it like this:
target_link_libraries(app PRIVATE textutils)
Yes, it may look a bit odd: we kind of “link” a header-only library. But in CMake target_link_libraries often means not only classic linking, but also connecting usage requirements, which include include paths, compile-definitions, and other properties.
Diagram: how include paths “travel” between targets
To avoid keeping everything in your head as magic, it's useful to imagine this as passing of requirements.
flowchart LR
A[calc target] -- PUBLIC include --> B[app target]
A -- PRIVATE src --> A
C[textutils INTERFACE] -- INTERFACE include --> B
The meaning of the diagram: PUBLIC and INTERFACE are what “goes outside” to the consumer. PRIVATE stays inside.
Practical rules for choosing PRIVATE / PUBLIC / INTERFACE
When you write your first CMakeLists.txt, it's very tempting to mark everything PUBLIC, because “that will surely work”. It will work, of course, but later you'll suddenly discover your project is like a kitchen with no doors: smells, noise, and pots in the living room, and guests somehow playing with the ladle.
A pragmatic strategy for a beginner is the following.
- For an executable (add_executable) include paths are almost always PRIVATE, because an executable is rarely used as a library by someone else.
- For a library (add_library) public headers from the include/ folder are usually added as PUBLIC, because otherwise consumers won't be able to use the library. Internal implementation folders (src/, src/detail/) are typically added as PRIVATE, so consumers don't depend on internals.
- For header-only libraries (add_library(name INTERFACE)) include paths are set as INTERFACE.
One more important psychological point: PUBLIC is a promise. If you mark a folder as PUBLIC, you are basically saying: “yes, a library user is allowed to rely on the headers located there”. And once you promised — you'll have to maintain it.
4. Common mistakes when working with target_include_directories
Mistake #1: trying to fix “header not found” by adding another #include in C++ code.
When the compiler says it couldn't find calc/sum.hpp, the problem is usually not that you “included too little”, but that the header search path is not configured. The fix should be in the CMakeLists.txt via target_include_directories, not chaotic additions of includes.
Mistake #2: marking things PUBLIC “to be safe”, without understanding what you're making public.
If you do target_include_directories(calc PUBLIC src), you're effectively telling consumers: “include my internal headers from src/; it's part of the API”. Later you may want to rename src/detail or restructure internals — and suddenly you'll break other people's code (or your own app, if it started poking into internals). It's much safer to keep src/ PRIVATE.
Mistake #3: expecting CMake to “automatically find include/”.
CMake isn't obligated to guess what you meant. If you have an include/ folder, it's not magical: until you add it via target_include_directories, the compiler may not know about it. Yes, some IDEs highlight headers and even provide autocomplete, but that doesn't mean the build is configured correctly.
Mistake #4: confusing include paths with the list of source files.
Adding an include directory makes the compiler able to find a header. But it doesn't add a .cpp to the build. So sometimes a project “sees declarations” but fails at linking (e.g., undefined reference). That's a different class of problem: headers are for compilation, whereas including .cpp files or linking libraries is about linking.
Mistake #5: mixing “public headers” and “implementation headers” in the same place.
If you put everything into include/, consumers begin to see implementation details. If you put public headers into src/, you end up having to expose src/ as PUBLIC, and the situation worsens. Separating include/ (interface) from src/ (implementation) may seem pedantic, but it's one of the most worthwhile pedantries in C++.
GO TO FULL VERSION