Tempate for a C++ cmake project using C++ 23 and modules.
Find a file Use this template
2026-09-09 14:55:45 +02:00
.vscode C++ with modules template 2026-09-08 11:19:36 +02:00
.zed fix zed config on Debian, spdlog 2026-09-08 17:20:39 +02:00
config lvgl demo 2026-09-09 14:41:12 +02:00
src clickable label 2026-09-09 14:55:45 +02:00
tests lvgl demo 2026-09-09 14:41:12 +02:00
.clangd fix zed config on Debian, spdlog 2026-09-08 17:20:39 +02:00
.gitignore logging module 2026-09-08 20:13:45 +02:00
CMakeLists.txt FTXUI CPU load demo 2026-09-09 14:22:22 +02:00
CMakePresets.json gtest 2026-09-09 13:20:25 +02:00
README.md lvgl demo 2026-09-09 14:41:12 +02:00

cpp_template

Template for a C++23 application built from named modules, with one library and one GoogleTest binary per module.

Requirements

Component Version Debian package
Clang 22 clang-22
CMake 4.3 or newer cmake
Ninja any ninja-build
spdlog any libspdlog-dev
FTXUI any libftxui-dev
GoogleTest any libgtest-dev
Xlib any libx11-dev

CMake 4.3 is required because the CMAKE_EXPERIMENTAL_CXX_IMPORT_STD UUID in CMakeLists.txt is bound to that version. Ninja is required because module dependency scanning does not work with the Makefile generators.

LVGL 9.5 and the lv:: C++ wrapper are not packaged for Debian and are fetched from git at configure time, so the first configure needs network access and git.

Layout

CMakeLists.txt              dependencies, warning flags, subdirectories
src/CMakeLists.txt          the app executable
src/modules/CMakeLists.txt  one static library per module
src/modules/*.cppm          module interface units
tests/CMakeLists.txt        one test executable per module
tests/*_test.cpp            the tests
config/lv_conf.h            LVGL configuration

Modules and their dependencies:

Module Library depends on Content
math nothing add, multiply
applog spdlog logger setup, level filtered log functions
cpuload nothing CPU utilization sampled from /proc/stat, load thresholds
tui math, cpuload, FTXUI, pthread live CPU dashboard
gui applog, math, cpuload, lv::lv, X11 the same dashboard as an LVGL window

Build

cmake --preset default
cmake --build --preset default
./build/default/app

The application prints a few log lines, then opens the live CPU dashboard of the tui module: a gauge for the current utilization and a chart of the last 240 samples, taken every 250 ms from /proc/stat. q or Esc leaves it and lets the gui module follow with the same content in an LVGL window: the math panel, a bar for the current load and a chart of the same history, at the same sampling rate and with the same color thresholds. Closing the window ends the application.

The window needs LV_X11_DIRECT_EXIT 0 in config/lv_conf.h. With the LVGL default of 1 the X11 driver calls exit(0) itself when the last window closes, and gui_demo() never returns.

Without an interactive terminal, for example when the output is piped, the dashboard prints a single frame instead of entering the event loop, so ./build/default/app | cat terminates on its own.

The application writes app.log into the working directory it is started from. The LVGL window is skipped when DISPLAY is unset.

Run the tests

The test binaries are built together with the application, so a plain build is enough to prepare them.

Run everything through CTest:

ctest --preset default

The preset already sets --output-on-failure. Without presets the same run is:

ctest --test-dir build/default --output-on-failure

Every TEST is registered as its own CTest case by gtest_discover_tests, so CTest reports 35 cases rather than 5 binaries. Useful variants:

ctest --test-dir build/default -N                 # list the cases, run none
ctest --test-dir build/default -R MathAdd         # regex over the case names
ctest --test-dir build/default -j 8               # run in parallel
ctest --test-dir build/default --rerun-failed     # only what failed last time

The regex of -R matches substrings, so -R Math also selects TuiDemo.PrintsTheValuesComputedByMath.

A single module can also be tested by calling its binary directly, which gives access to the GoogleTest options:

./build/default/tests/math_test
./build/default/tests/applog_test --gtest_filter='AppLogTest.Writes*'
./build/default/tests/tui_test --gtest_list_tests
./build/default/tests/gui_test --gtest_repeat=10 --gtest_shuffle

The five binaries are math_test, cpuload_test, applog_test, tui_test and gui_test. applog_test creates and deletes app.log in its working directory. gui_test covers the label formatting and the DISPLAY guard of gui_demo, not the LVGL event loop, which cannot run unattended. The same split applies to tui_test: it renders frames at a fixed size through tui::render_dashboard and checks the non-interactive path of tui_demo, while the event loop itself is not covered.

Tests can be excluded from the build entirely:

cmake --preset default -DBUILD_TESTING=OFF

Adding a module

  1. Write src/modules/<name>.cppm with export module <name>;.

  2. Add the library to src/modules/CMakeLists.txt:

    add_library(<name> STATIC)
    target_sources(<name>
      PUBLIC
        FILE_SET CXX_MODULES FILES <name>.cppm
    )
    target_link_libraries(<name> PRIVATE project_options)
    set_property(TARGET <name> PROPERTY CXX_MODULE_STD ON)
    

    The file set has to be PUBLIC, otherwise the module cannot be imported by other targets. CXX_MODULE_STD is a target property and is not inherited, so it is set on every target, including the test executables.

  3. Link the new library wherever the module is imported: add <name> to the target_link_libraries of app when main.cpp imports it, or to the module library that imports it.

  4. Write tests/<name>_test.cpp and register it in tests/CMakeLists.txt:

    add_module_test(<name>_test <name>)
    

    The helper links the module library, GTest::gtest_main and the shared warning flags, and registers the cases with CTest.

Editor integration

.vscode/ and .zed/ contain configure, build and run tasks plus a clangd setup with --experimental-modules-support. clangd reads build/default/compile_commands.json, so the project has to be configured once before diagnostics are correct.

Notes on modules

import std; is still experimental in CMake and prints a warning at generate time. Header libraries without module support, currently LVGL and lv::, are included in the global module fragment of the module that wraps them; CMAKE_CXX_SCAN_FOR_MODULES is switched off around their FetchContent call because scanning them only costs build time.