Site

Demonstrations — Building Demo Executables with CMake

Tutorial D1.0  •  C++ / Learn / Demonstrations

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:

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
CommandWhat it does
cmake -S . -B buildConfigure: read CMakeLists.txt, generate build files in build/
cmake --build buildBuild all targets: demo_lib, basic, words, shapes
cmake --build build --target basicBuild only the basic executable
cmake --build build --config ReleaseRelease 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

TermMeaning
add_libraryCMake command that declares a library target
add_executableCMake command that declares an executable target
add_subdirectoryIncludes another directory's CMakeLists.txt in the build
target_link_librariesLinks a target against a library; propagates PUBLIC properties
target_include_directoriesSets include search paths; PUBLIC propagates to dependents
foreachCMake loop construct that iterates over a list of values
static libraryArchive of object files linked into each executable at build time
#pragma onceInclude guard preventing a file from being processed more than once