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) andsrc/
(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 andprivate/
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
- With extensions of
- Functional/Integration tests: placed under
tests/
- with a subdirectory per scenario
tests/basics/driver.cpp
ortests/communication/driver.cpp
- build to a single executable per test
- with a subdirectory per scenario
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 undertests/
with structured subdirectories.Strict Compiler Warnings and Out-of-Source Enforcement
Centralized warnings viaCompilerWarnings.cmake
with toggleableWARNINGS_AS_ERRORS
and prevention of in-source builds viaPreventInSourceBuilds.cmake
for a clean workspace.cmake/StandardProjectSettings.cmake
provides default build type, colored diagnostics, etc.Modern CMake Practices
UsesFILE_SET
for public headers, interface targets for warnings, andFetchContent
for dependencies.Toolchain and Cross-Compilation Ready
Dedicatedtoolchains/
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