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.
sudo pacman -S cmake ninja clangStart your project and prepare for building
The project structure should resemble:
.├── build.sh├── CMakeLists.txt├── compile_commands.json # Auto generated by CMake└── src └── main.cppBasic Setup
Create a new project directory and navigate into the newly created project directory.
mkdir modern_cppcd modern_cppCMake
Create and edit a CMakeLists.txt file to define your project’s build configuration.
nvim CMakeLists.txtTo properly use modules, C++23, and import std, it is recommended to configure your CMakeLists.txt as follows:
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:
nvim build.shchmod +x build.shHere’s mine, you can use it freely.
#!/bin/bash
set -e
BUILD_DIR="build"EXECUTABLE="main"
show_help() { cat << EOFUsage: $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 0fi
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:
nvim .clangdCompileFlags: Add: - -std=c++23 - -stdlib=libc++ - -Wno-unknown-attributes Compiler: clang++
Index: Background: BuildHello World
Can’t wait to have a taste of modern C++? Let’s begin with a Hello World but quite differently.
mkdir srcnvim src/main.cppimport 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.
./build.sh -S # It's how my script is usedHello WorldAmazed? 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:
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;}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.
./build.sh -SHello, C++23 & Modules!> 102 3 5 7 11 13 17 19 23 29This 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!