If you’re starting a new C or C++ project, be it a library or software package, Canonical Project Structure (P1204R0) is an excellent choice for modern workflows in my opinion. Its aim is to provide a

…source code layout and content guidelines for new C++ projects that would facilitate their packaging.

There are plenty of widely used project layouts with their own philosophy. These include, but are not limited to:

  • Autotools
    With it’s classic approach for portable builds using GNU Autotools.

  • Bazel
    For structuring and building C++ projects with multiple packages and targets.

  • Meson
    A build system with integrated testing, linting, and benchmarking.

  • Pitchfork
    A set of conventions for modern C++ projects, with it’s own project layout.

In this post I will first guide you through the layout, then share the benefits and finally how I utilize it my projects.


Structure

Before rapidly going through the key points of the structure, let’s look at its file system structure of a simple project:

libhello/                             # project root directory
├── libhello/                         # source subdirectory         
├── tests/                            # tests subdirectory

Each project has a project root directory that is named after the project (here libhello) which is then followed by a directory of a same name known as source directory. As the name suggests this is where all the source code for the project goes on here. If a project has any functional/integration tests they are placed under the tests subdirectory.

Projects root directory also is the home for all the build system files, README, LICENSE etc. and other directories such as docs/ as shown on the example above.

Naming and includes

  • Grouped source code does not split include/ (headers) and src/ (sources), but rather keeps headers and sources together
  • File extensions
    • .h/.c for C source code
    • .hpp/.cpp for C++ source code
    • .ipp/.mpp/.tpp for inlines, modules and templates
  • Include: using only the angle brackets and project prefix #include <libhello/hello.hpp> making includes with quotation #include "hello.hpp" prohibited
  • Internal structure: use details/ for implementation details and private/ for truly private headers if needed

Tests layout

  • Unit tests: are placed next to the source code they test
    • With extensions of *.test.c and *.test.cpp
    • Should not be compiled into the library target
  • Functional/Integration tests: placed under tests/
    • with a subdirectory per scenario tests/basics/driver.cpp or tests/communication/driver.cpp
    • build to a single executable per test

Benefits

Using points mentioned above the final result of the C++ project called libhello would looks like this:

libhello/
├── libhello/
│   ├── hello.hpp
│   ├── hello.cpp
│   └── hello.test.cpp
├── tests/
    └── basics/
        └── driver.cpp

Keeping this layout in mind, here are key benefits:

  • Predictable and package‑friendly layout: tooling and users are aware of source code location
    • Easier to build and package across ecosystems
  • Faster navigation and modularization‑ready: grouping headers and sources simplifies editing, code search, and maps naturally to C++ modules and generated code
  • Robust includes: project‑prefixed <...> prevents header collisions and supports installation to system include paths
  • Clean testing model
    • unit tests located close to the implementation
    • integration tests are run via public APIs from tests/ simulating actually usage of the project

CMake - Structure In Action

My template project demonstrates a clean, modern CMake-based structure with Canonical Project Structure (P1204R0) principles. It uses CMake presets, toolchain files, and modular cmake helpers for reproducible builds and easy CI integration.

libhello/
├── cmake/
│   ├── CompilerWarnings.cmake
│   ├── PreventInSourceBuilds.cmake
│   └── StandardProjectSettings.cmake
├── libhello/
│   ├── hello.hpp
│   ├── hello.cpp
│   └── hello.test.cpp
├── tests/
│   └── basics/
│       └── driver.cpp
├── toolchains/
│   └── default.cmake
├── .clang-format
├── .cmake-format.yaml
├── .gitignore
├── .gitmodules
├── CMakeLists.txt
├── CMakePresets.json
└── README.md

Key Features

  • Full P1204R0 Compliance
    Headers and sources grouped under a single source subdirectory, project-prefixed includes, and .test.cpp convention for unit tests. Functional/integration tests under tests/ with structured subdirectories.

  • Strict Compiler Warnings and Out-of-Source Enforcement
    Centralized warnings via CompilerWarnings.cmake with toggleable WARNINGS_AS_ERRORS and prevention of in-source builds via PreventInSourceBuilds.cmake for a clean workspace. cmake/StandardProjectSettings.cmake provides default build type, colored diagnostics, etc.

  • Modern CMake Practices
    Uses FILE_SET for public headers, interface targets for warnings, and FetchContent for dependencies.

  • Toolchain and Cross-Compilation Ready
    Dedicated toolchains/ directory for portable builds across platforms and compilers.

  • Code Quality and Formatting
    .clang-format and .cmake-format.yaml ensure consistent style across the codebase.

  • Future-Proof for Modules
    Layout and naming conventions align with C++20 modules and modern packaging workflows.

  • Preset-Driven Builds
    CMakePresets.json for reproducible, IDE-friendly configurations and CI/CD pipelines.

Closing thoughts

I chose this structure for all my future C and C++ projects as it strikes the perfect balance between clarity, scalability, and modern C++ practices. By grouping headers and sources, it simplifies navigation and supports future C++20 modules. Preset-driven builds make CI/CD pipelines predictable and reproducible, while strict compiler warnings and out-of-source enforcement keep the codebase clean and maintainable. With integrated testing, toolchain flexibility, and a layout aligned with P1204R0, this approach ensures my projects are robust, portable, and ready for long-term growth.

Bonus: Getting Started with the Template

# Clone the template into libhello/
git clone --recurse-submodules \
https://gitlab.com/sorhanp/canonical-project-structure-template.git \
libhello

# Enter the project directory
cd libhello

# Configure using CMake presets (Debug build)
cmake --preset gcc-debug

# Build the project
cmake --build --preset gcc-debug-build

# Run unit tests
ctest --preset gcc-debug-test