D1.0 What This Teaches
CMake lets you build a static library and attach multiple demo executables to it in
the same project tree, without writing a separate build file for each demo. This
tutorial covers:
- Splitting a CMake project into a library and a
demos/ subdirectory
- Using
add_subdirectory to compose the build from parts
- A
foreach loop in CMake to register many executables with one rule
- Linking each demo against the library with
target_link_libraries
- Configuring, building, and running individual demos
D1.1 Why a demos/ Directory?
A static library has no entry point - you cannot run it directly. A demos/
directory collects small standalone programs that link against the library and show how
to call its API. Keeping demonstration code here separates it from the library sources
and from automated tests.
The pattern mirrors what Cargo's examples/ directory does for Rust: each
demo compiles to its own executable, and all demos are built together by the same
top-level build command.
D1.2 Project Layout
Demonstrations/
├── CMakeLists.txt ← root: defines the library, pulls in demos/
├── include/
│ └── demo_lib.h ← public declarations for the library
├── src/
│ └── demo_lib.cpp ← library implementation
└── demos/
├── CMakeLists.txt ← adds one executable per demo source file
├── basic.cpp
├── words.cpp
└── shapes.cpp
Library sources live in src/; public declarations live in
include/. Each .cpp in demos/ becomes its
own named executable.
D1.3 The Library Header - include/demo_lib.h
Every symbol the demos use must be declared here:
// demo_lib.h - public API for the Demonstrations library.
#pragma once
#include <string>
int add(int a, int b);
int clamp(int value, int lo, int hi);
int word_count(const std::string& s);
struct Circle {
double radius;
Circle(double r) : radius(r) {}
double area() const;
double circumference() const;
};
struct Rectangle {
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
double area() const;
double perimeter() const;
bool is_square() const;
};
#pragma once is a universally supported include guard that prevents the
header from being processed more than once per translation unit.
D1.4 The Library Implementation - src/demo_lib.cpp
// demo_lib.cpp - implementation of the Demonstrations library.
#include "demo_lib.h"
#include <numbers>
#include <sstream>
int add(int a, int b) { return a + b; }
int clamp(int value, int lo, int hi) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
int word_count(const std::string& s) {
std::istringstream stream(s);
std::string word;
int count = 0;
while (stream >> word) ++count;
return count;
}
double Circle::area() const { return std::numbers::pi * radius * radius; }
double Circle::circumference() const { return 2.0 * std::numbers::pi * radius; }
double Rectangle::area() const { return width * height; }
double Rectangle::perimeter() const { return 2.0 * (width + height); }
bool Rectangle::is_square() const { return width == height; }
std::numbers::pi requires C++20. The root CMakeLists.txt
sets CMAKE_CXX_STANDARD 20 for the entire project.
D1.5 Root CMakeLists.txt
# CMakeLists.txt - root build file for the Demonstrations project.
cmake_minimum_required(VERSION 3.20)
project(Demonstrations LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(demo_lib src/demo_lib.cpp)
target_include_directories(demo_lib PUBLIC include)
add_subdirectory(demos)
add_library creates a static library target named demo_lib.
target_include_directories(... PUBLIC include) propagates the
include/ path to any target that links against demo_lib,
so demos find the header without specifying it themselves.
add_subdirectory(demos) reads demos/CMakeLists.txt and
adds its targets to the build.
D1.6 demos/CMakeLists.txt
# demos/CMakeLists.txt - one executable per demo source file.
foreach(demo basic words shapes)
add_executable(${demo} ${demo}.cpp)
target_link_libraries(${demo} PRIVATE demo_lib)
endforeach()
The foreach loop iterates basic words shapes. For each name
it calls add_executable with that name and the matching .cpp
file, then links the executable against demo_lib.
Adding a new demo requires only adding its name to the list and creating the
corresponding .cpp file.
D1.7 Demo: basic.cpp
// basic.cpp - demonstrates add and clamp from demo_lib.
// Build the project, then run: ./build/demos/basic
#include <iostream>
#include "demo_lib.h"
int main() {
std::cout << "=== basic demo ===\n";
std::cout << "add(7, 3) = " << add(7, 3) << "\n";
std::cout << "clamp(25, 0, 20) = " << clamp(25, 0, 20) << "\n";
std::cout << "clamp(-5, 0, 20) = " << clamp(-5, 0, 20) << "\n";
std::cout << "clamp(10, 0, 20) = " << clamp(10, 0, 20) << "\n";
}
Because demo_lib uses a PUBLIC include directory,
#include "demo_lib.h" resolves without any extra compiler flags in the
demo's own build rule.
D1.8 Demo: words.cpp
// words.cpp - demonstrates word_count from demo_lib.
// Build the project, then run: ./build/demos/words
#include <iostream>
#include <string>
#include "demo_lib.h"
int main() {
std::cout << "=== words demo ===\n";
std::string text = "the quick brown fox jumps over the lazy dog the fox";
std::cout << "text: \"" << text << "\"\n";
std::cout << "word_count: " << word_count(text) << "\n";
}
D1.9 Demo: shapes.cpp
// shapes.cpp - demonstrates Circle and Rectangle from demo_lib.
// Build the project, then run: ./build/demos/shapes
#include <format>
#include <iostream>
#include "demo_lib.h"
int main() {
std::cout << "=== shapes demo ===\n";
Circle c(5.0);
std::cout << std::format("Circle r=5:\n area = {:.4f}\n"
" circumference = {:.4f}\n",
c.area(), c.circumference());
Rectangle r(4.0, 6.0);
std::cout << std::format("Rectangle 4x6:\n area = {:.1f}\n"
" perimeter = {:.1f}\n is_square = {}\n",
r.area(), r.perimeter(), r.is_square());
Rectangle sq(5.0, 5.0);
std::cout << std::format("Rectangle 5x5:\n is_square = {}\n",
sq.is_square());
}
std::format is a C++20 feature that works like Python's f-strings:
{:.4f} formats a floating-point value to four decimal places.
D1.10 Build and Run
Configure and build from the Demonstrations/ directory:
cmake -S . -B build
cmake --build build
Run a demo on Linux or macOS:
./build/demos/basic
./build/demos/words
./build/demos/shapes
Run a demo on Windows (MSVC debug build):
build\demos\Debug\basic.exe
| Command | What it does |
cmake -S . -B build | Configure: read CMakeLists.txt, generate build files in build/ |
cmake --build build | Build all targets: demo_lib, basic, words, shapes |
cmake --build build --target basic | Build only the basic executable |
cmake --build build --config Release | Release build - optimized, no debug symbols |
D1.11 Expected Outputs
./build/demos/basic
=== basic demo ===
add(7, 3) = 10
clamp(25, 0, 20) = 20
clamp(-5, 0, 20) = 0
clamp(10, 0, 20) = 10
./build/demos/words
=== words demo ===
text: "the quick brown fox jumps over the lazy dog the fox"
word_count: 11
./build/demos/shapes
=== shapes demo ===
Circle r=5:
area = 78.5398
circumference = 31.4159
Rectangle 4x6:
area = 24.0
perimeter = 20.0
is_square = false
Rectangle 5x5:
is_square = true
D1.12 Exercise
Exercise
- Add a
unique_words(const std::string& s) -> std::vector<std::string>
function to the library. Sort the words and remove duplicates before returning.
- Add a
stats demo in demos/stats.cpp that calls both
word_count and unique_words on several strings and
prints both results.
- Add
stats to the foreach list in
demos/CMakeLists.txt. Re-run the configure step, rebuild, and run
the new demo.
D1.13 Common Mistakes
Forgetting to add the demo name to the foreach list
Creating demos/stats.cpp without adding stats to the
foreach list means CMake never defines a build target for it. Nothing
fails - the file is silently ignored.
PRIVATE vs PUBLIC for include directories
target_include_directories(demo_lib PRIVATE include) keeps the path
inside the library only. Demos linking against it would then need their own
target_include_directories. Using PUBLIC on the library
propagates the path automatically to all dependents.
Not re-running cmake after changing CMakeLists.txt
Adding a name to the foreach list and then only running the build step
may not pick up the change. Re-run cmake -S . -B build whenever a
CMakeLists.txt changes.
Defining function bodies in the header
Writing a function body directly in the header (not just a declaration) without
inline gives every translation unit that includes it a copy. The
linker then reports "multiple definition" errors. Declare in the header, define in
.cpp, or mark the body inline.
D1.14 Key Terms
| Term | Meaning |
| add_library | CMake command that declares a library target |
| add_executable | CMake command that declares an executable target |
| add_subdirectory | Includes another directory's CMakeLists.txt in the build |
| target_link_libraries | Links a target against a library; propagates PUBLIC properties |
| target_include_directories | Sets include search paths; PUBLIC propagates to dependents |
| foreach | CMake loop construct that iterates over a list of values |
| static library | Archive of object files linked into each executable at build time |
| #pragma once | Include guard preventing a file from being processed more than once |