# A Guide to Modern C++23 Projects with Modules and Functional Programming

5 min read
Table of Contents

If your C++ feels like a messy maze of headers and hard-to-follow loops, it’s time for a change. Modern C++23 provides a powerful duo to tackle this: modules for organization and functional programming for clarity. Stop describing how to compute, and start declaring what you mean.

Necessary Packages

We need CMake 3.30+, Ninja 1.12.0+, Clang 18.0+ to enjoy some latest and modern features.

Terminal window
sudo pacman -S cmake ninja clang

Start your project and prepare for building

The project structure should resemble:

.
├── build.sh
├── CMakeLists.txt
├── compile_commands.json # Auto generated by CMake
└── src
└── main.cpp

Basic Setup

Create a new project directory and navigate into the newly created project directory.

Terminal window
mkdir modern_cpp
cd modern_cpp

CMake

Create and edit a CMakeLists.txt file to define your project’s build configuration.

Terminal window
nvim CMakeLists.txt

To properly use modules, C++23, and import std, it is recommended to configure your CMakeLists.txt as follows:

CMakeLists.txt
cmake_minimum_required(VERSION 3.30)
project(ModernCPP VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_compile_options(-stdlib=libc++)
add_link_options(-stdlib=libc++ -lc++abi)
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/modules)
set(STD_PCM ${CMAKE_BINARY_DIR}/modules/std.pcm)
add_custom_command(
OUTPUT ${STD_PCM}
COMMAND clang++ -std=c++23 -stdlib=libc++ --precompile
-o ${STD_PCM} /usr/share/libc++/v1/std.cppm
VERBATIM
)
add_custom_target(std_module ALL DEPENDS ${STD_PCM})
file(GLOB_RECURSE MODULE_SOURCES src/*.cppm)
if(MODULE_SOURCES)
add_library(project_modules)
target_sources(project_modules PUBLIC FILE_SET CXX_MODULES FILES ${MODULE_SOURCES})
target_compile_options(project_modules PUBLIC
-fmodule-file=std=${STD_PCM}
-fprebuilt-module-path=${CMAKE_BINARY_DIR}/modules
)
add_dependencies(project_modules std_module)
endif()
file(GLOB_RECURSE CPP_SOURCES src/*.cpp)
if(NOT CPP_SOURCES)
message(FATAL_ERROR "No .cpp files found in src/")
endif()
add_executable(main ${CPP_SOURCES})
target_compile_options(main PRIVATE
-fmodule-file=std=${STD_PCM}
-fprebuilt-module-path=${CMAKE_BINARY_DIR}/modules
)
add_dependencies(main std_module)
if(MODULE_SOURCES)
target_link_libraries(main PRIVATE project_modules)
endif()
add_custom_command(TARGET main POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_BINARY_DIR}/compile_commands.json
${CMAKE_SOURCE_DIR}/compile_commands.json
)

Build Script

We can also create a practical build script:

Terminal window
nvim build.sh
chmod +x build.sh

Here’s mine, you can use it freely.

build.sh
#!/bin/bash
set -e
BUILD_DIR="build"
EXECUTABLE="main"
show_help() {
cat << EOF
Usage: $0 [OPTION]
Build Operations:
-Sr Build release
-Sd Build debug
-Sdd Build debug and start debugger (gdb)
-S Build and run (default: release)
Runtime Operations:
-D Start debugger (gdb) with existing binary
Query Operations:
-Q Show build info
Maintenance Operations:
-C Clean build artifacts
EOF
}
query_info() {
echo "==> Project Information"
if [ -d "$BUILD_DIR" ]; then
echo "Build directory: $BUILD_DIR"
if [ -f "$BUILD_DIR/CMakeCache.txt" ]; then
BUILD_TYPE=$(grep CMAKE_BUILD_TYPE:STRING "$BUILD_DIR/CMakeCache.txt" | cut -d'=' -f2)
echo "Build type: ${BUILD_TYPE:-Unknown}"
COMPILER=$(grep CMAKE_CXX_COMPILER:FILEPATH "$BUILD_DIR/CMakeCache.txt" | cut -d'=' -f2)
echo "Compiler: ${COMPILER:-Unknown}"
fi
if [ -f "$BUILD_DIR/$EXECUTABLE" ]; then
SIZE=$(du -h "$BUILD_DIR/$EXECUTABLE" | cut -f1)
echo "Binary size: $SIZE"
echo "Binary: $BUILD_DIR/$EXECUTABLE"
else
echo "Binary: Not built"
fi
else
echo "Not configured yet"
fi
}
clean_build() {
echo "==> Cleaning build artifacts"
rm -rf "$BUILD_DIR" compile_commands.json
echo "==> Clean complete"
}
build_project() {
local build_type=$1
echo "==> Building ($build_type)"
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
cmake -GNinja \
-DCMAKE_BUILD_TYPE="$build_type" \
-DCMAKE_CXX_COMPILER=clang++ \
..
ninja
cd ..
echo "==> Build complete"
}
run_binary() {
if [ ! -f "$BUILD_DIR/$EXECUTABLE" ]; then
echo "error: binary not found. Build first with -Sr or -Sd"
exit 1
fi
echo "==> Running $EXECUTABLE"
"./$BUILD_DIR/$EXECUTABLE"
}
debug_binary() {
if [ ! -f "$BUILD_DIR/$EXECUTABLE" ]; then
echo "error: binary not found. Build debug version with -Sd first"
exit 1
fi
echo "==> Starting debugger"
gdb "./$BUILD_DIR/$EXECUTABLE"
}
if [ $# -eq 0 ]; then
show_help
exit 0
fi
case "$1" in
-Sr)
build_project "Release"
;;
-Sd)
build_project "Debug"
;;
-Sdd)
build_project "Debug"
debug_binary
;;
-S)
build_project "Release"
run_binary
;;
-D)
debug_binary
;;
-Q)
query_info
;;
-C)
clean_build
;;
-h|--help)
show_help
;;
*)
echo "error: unknown option '$1'"
echo "Try '$0 --help' for more information"
exit 1
;;
esac

.clangd

Also, it’s recommended to config your clangd with .clangd for a better developing experience:

Terminal window
nvim .clangd
.clangd
CompileFlags:
Add:
- -std=c++23
- -stdlib=libc++
- -Wno-unknown-attributes
Compiler: clang++
Index:
Background: Build

Hello World

Can’t wait to have a taste of modern C++? Let’s begin with a Hello World but quite differently.

Terminal window
mkdir src
nvim src/main.cpp
src/main.cpp
import std;
int main() {
std::println("Hello World");
return 0;
}

And then you can use your build script to build and run to see the result.

Terminal window
./build.sh -S # It's how my script is used
Output
Hello World

Amazed? Check more articles to learn about these modern cpp features.

Modules

You can also use modules to organize different codes. And here is a more complicated example:

src/main.cpp
import std;
import hello;
int main() {
namespace rg = std::ranges;
namespace vs = std::views;
hello::greet("C++23 & Modules");
auto primes = vs::iota(2ull) | hello::primes;
rg::copy(primes | vs::take(*std::istream_iterator<std::size_t>(std::cin)), std::ostream_iterator<int>{std::cout, " "});
std::cout << std::endl;
return 0;
}
src/hello.cppm
export module hello;
import std;
export namespace hello {
namespace rg = std::ranges;
namespace vs = std::views;
void greet(std::string_view name) {
std::println("Hello, {}!", name);
}
auto primes = vs::filter([](const auto x) {
return x > 1 && rg::none_of(
vs::iota(static_cast<decltype(x)>(2), static_cast<decltype(x)>(std::sqrt(x)) + 1),
[x](const auto i) { return x % i == 0; }
);
});
}

This elegant code first outputs “Hello C++23 & Modules”, then accepts an integer input ‘n’ and outputs the first n prime numbers. You can proceed to build and run it.

Terminal window
./build.sh -S
Output
Hello, C++23 & Modules!
> 10
2 3 5 7 11 13 17 19 23 29

This simple example demonstrates many new features such as modules, and functional programming with ranges and views. May this example inspire you to begin exploring modern C++ programming!

My avatar

Positron

has a spin of ½