If you’ve ever stared at a blank folder wondering where do I even start?, you’re not alone. I’ve been there—more than once.

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.

Over time, I discovered that a good project structure is like a well-organized workshop: tools where you expect them, clear labels, and no mystery boxes.

In this post, I’ll walk you through the Canonical Project Structure (inspired by P1204R0), why I love it, and how I use it with modern CMake. Think of this as both a guide and a story from my own experience.


Structure

Here’s the big picture: your project should feel predictable. When someone opens it, they should instantly know where the source lives, where tests hide, and where docs whisper their secrets.

A minimal example looks like this:

libhello/
├── libhello/        # source code (headers + sources together)
├── tests/           # integration tests

Why this layout? Because splitting include/ and src/ often creates unnecessary friction. Instead, keep headers and sources side by side—it’s faster to navigate and future-proof for modules.

Naming tips I swear by:

  • .hpp/.cpp for C++
  • .h/.c for C
  • Use <libhello/hello.hpp> for includes—never quotes.
  • For internals, tuck them under details/ or private/.

Tests Layout

Unit tests? Keep them close to the code they test, like a loyal sidekick:

libhello/
├── hello.cpp
├── hello.test.cpp

Integration tests? Give them their own playground under tests/ with subfolders per scenario.

Why This Rocks

  • Predictable layout = happy tooling and happier humans
  • Easier packaging across ecosystems
  • Headers + sources together = less hunting, more coding
  • Clean testing model: unit tests nearby, integration tests realistic

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

CMake – Structure in Action

Instead of drowning in boilerplate, here’s what makes my setup shine:

  • FetchContent for dependencies: No submodules, no manual cloning—CMake handles it all.
  • Toolchain flexibility: Automatically fetches and applies toolchain files for consistent builds.
  • Strict compiler warnings: Centralized via helper modules, with easy toggles for WARNINGS_AS_ERRORS.
  • Preset-driven builds: Reproducible configurations for local dev and CI/CD.
  • Integrated testing: GoogleTest pulled in via FetchContent, ready for unit and integration tests.
  • Future-proof: Layout aligns with C++20 modules and modern packaging workflows.

Getting Started

Ready to roll? Here’s the quick start guide:

# Clone the template into libhello/
git clone \
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

That’s it—no submodules, no headaches. Just modern CMake doing its thing.

Closing Thoughts

This structure isn’t just theory—it’s something I use every day. It keeps my projects clean, portable, and ready for the future. If you’ve been struggling with messy layouts or brittle builds, give this a try.

And remember: a good structure is like a good habit—it pays off every single time you build.

-sorhanp