Organizing a C++ Project

Learn how to structure your C++ projects for scalability and maintainability, from basic directory layouts to managing multiple modules.

Greg Filak
Published

In the previous chapter, we dissected the C++ build process. We followed our code on its journey from human-readable source files to machine-code-filled object files, and finally watched the linker stitch everything together into a runnable program.

Now, we need to zoom out. Before we write a single line of C++ or configure a build system, we need a plan. Where do our files live? How do we keep our source code from becoming a tangled mess?

A well-organized project is a joy to work on: it's easy to navigate, simple to build, and straightforward to scale. A messy project, on the other hand, is a recipe for headaches, bugs, and wasted time.

In this lesson, we'll establish a solid foundation by exploring standard conventions for project layouts. We'll learn where to put source files, header files, libraries, and tests, and how to structure a project that can grow from a single executable into a complex system of interconnected libraries.

Typical Project Directory Layout

When you start a new project, it's tempting to just throw all your files into one big folder. This works for a program with two or three files, but it breaks down almost immediately. As soon as you add tests, external libraries, or build artifacts, your root directory becomes a chaotic junk drawer.

To bring order to this chaos, we typically converge on a few common-sense conventions for directory structures. While there's no single, universally agreed standard, most well-structured projects look something like this:

my_game/
├─ src/
├─ include/
├─ libs/
├─ build/
├─ tests/
└─ docs/

Let's break down the purpose of each of these directories.

Source Files

The source/ or src/ directory is the heart of your project. This is where your .cpp files live. These files contain the implementation details: the bodies of your functions, the definitions of your class methods, and the core logic of your application.

By keeping all your implementation files in one place, you create a clear separation between what your code does (its interface, defined in headers) and how it does it (its implementation, defined in source files).

Include Files

If src/ is the "how", the include/ directory is the "what". This folder contains the public header files (.h or .hpp) for your project. These headers define the public API (Application Programming Interface) of your libraries and executables. They contain declarations for classes, functions, and variables that you want other parts of your project -or even external projects - to be able to use.

Separating public headers into an include/ directory makes your project's interface explicit. Anyone looking at this folder can immediately understand what functionality your project exposes to the outside world.

External Library Files

Few C++ projects exist in a vacuum. You'll almost certainly use external libraries for everything from graphics (SDL, SFML) and networking (Boost.Asio, cpr) to data serialization (nlohmann/json, Cereal).

The libs/ or external/ directory is the conventional place to put this external code. Whether you're including the source code directly or just storing pre-compiled library files (.a, .lib, .so, .dll) and their headers, keeping them in a dedicated folder prevents third-party code from getting mixed up with your own.

Build Output

This is perhaps the most important directory for maintaining a clean project. The build/ directory is where all the output of your build process should go. This includes:

  • Object files (.o, .obj)
  • Library files (.a, .lib, .so, .dll)
  • Executables
  • Build system files (e.g., Makefiles, Visual Studio solutions)

By directing all build artifacts into a separate build/ directory, you practice what's known as an out-of-source build. Your original source tree (src/, include/, etc.) remains pristine and untouched. If you want to clean your project, you can just delete the build/ directory and start over.

This separation is a fundamental concept that we will emphasize throughout this course, as it's a cornerstone of modern CMake usage.

Other Common Directories

  • tests/: For automated test scripts. We'll cover automated testing later in the course
  • docs/: For documentation files, explaining how the project works.
  • assets/ or data/: For non-code resources your application needs at runtime, such as images, fonts, configuration files, or 3D models.
  • tools/: For utility scripts or small programs used during development, but not part of the final product (e.g., a script to format code or generate asset files).

Adopting a structure like this from the very beginning will pay dividends in the long run, making your project vastly more organized and professional.

The project root typically also contains a few important files. Typically these are files that allow the project to be compiled, such as a vcxproj file if you're using Visual Studio, or a CMakeLists.txt file if you're using CMake.

Header and Source File Organization

Now that we have a high-level directory structure, let's zoom in on the relationship between src/ and include/.

The .h/.cpp Pair

As a general rule, every .cpp file that defines significant functionality should have a corresponding .h file.

  • The header file (.h or .hpp) defines the interface. It's like a contract, telling the world, "I provide a class named Player with these public methods."
  • The source file (.cpp) provides the implementation. It fulfills the contract, containing the actual code for the Player class methods.

For example, a Player component in a game would have:

  • include/Player.h: Contains the class Player { ... }; definition.
  • src/Player.cpp: Contains the implementations, like Player::update() { ... }.

Any other part of your code that needs to interact with a Player will simply #include "Player.h". It doesn't need to know about Player.cpp - that's a detail the linker will handle later.

Public vs. Private Headers

Not all headers are created equal. Some headers define the public-facing API of a library, while others are internal implementation details that should not be used by outside code.

This leads to a common organizational pattern:

  • Public headers go in include/. These are the headers you intend for consumers of your library to use.
  • Private headers stay in src/. These headers are only included by the .cpp files within the same module. They are not part of the public interface.

Let's refine our Player example. Imagine the Player class uses a helper class, AnimationState, that no one else needs to know about.

The directory structure might look like this:

my_game/
├─ include/
│ └─ Player.h
└─ src/
  ├─ Player.cpp
  └─ AnimationState.h

In this case, Player.cpp would include both headers:

src/Player.cpp

// Include our own public header
#include "Player.h"

// Include the private helper header
#include "AnimationState.h"

// ... implementation of Player methods ...

This convention makes your library's public API crystal clear. Anything in include/ is fair game for users; anything in src/ is an implementation detail that can be changed without breaking client code.

For this reason, many projects are structured with public/ and private/ directories, instead of the include/ and src/ pattern we're describing here.

Include Path Best Practices

How you write your #include directives matters. A common practice is to structure your include directory to mirror your project's modules or namespaces and to use those paths in your include directives.

For example, if your Player class was part of a Game namespace, instead of placing the header at include/Player.h, you might use include/Game/Player.h. Then, a file that includes this header might do so like this:

#include "Game/Player.h"

void SomeFunction() {
  Game::Player SomePlayer;
  // ...
}

This has two major benefits:

  1. It avoids name collisions. What if you use a third-party library that also has a Player.h? By namespacing your includes with a project-specific folder, you eliminate ambiguity.
  2. It makes dependencies explicit. Seeing #include "Game/Player.h" immediately tells the reader that this code depends on the Game module of your project.

To make this work, you just need to tell your compiler (or, later, CMake) to add the include/ directory to its list of search paths.

Managing Multiple Modules or Libraries

The structure we've discussed works great for a single library or executable. But real-world projects are rarely that simple. A typical application might be composed of:

  • A core library containing fundamental data structures and logic.
  • A physics library.
  • A rendering library.
  • A main application executable that ties them all together.

Organizing a project like this requires breaking it down into logical modules or components. Each module can be thought of as a mini-project that builds into its own library.

Our directory structure can be extended to handle this. A common approach is to create subdirectories within src/ and include/ for each module:

my_game/
├─ include/
│ ├─ core/
│ │ └─ GameState.h
│ └─ graphics/
│   └─ Renderer.h
└─ src/
  ├─ core/
  │ ├─ GameState.cpp
  │ └─ private_core_helper.h
  └─ graphics/
    └─ Renderer.cpp

Or, alternatively, to create subdirectories within the root project for each module:

my_game/
├─ core/
│ ├─ include/
│ │ └─ GameState.h
│ └─ src/
│   ├─ GameState.cpp
│   └─ private_core_helper.h
└─ graphics/
  ├─ include/
  │ └─ Renderer.h
  └─ src/
    └─ Renderer.cpp

Either of these modular approaches has a few benefits:

  • Separation of Concerns: Each module has a single, well-defined responsibility. The core team doesn't need to know the details of how the graphics team implements rendering.
  • Faster Recompilation: If you only change a file in the graphics library, you only need to recompile that library and then re-link the final application. You don't need to recompile the core library.
  • Reusability: You could easily take the graphics library from this project and reuse it in a different game.

This is the physical file layout that modern CMake is designed to work with. In the upcoming chapters, you will learn the CMake commands like add_library(), add_executable(), and target_link_libraries() that map directly onto this clean, modular directory structure.

Summary

Before diving into a build system, establishing a sane project structure is important. It's the architecture of your codebase at the filesystem level, and getting it right makes everything that follows - compiling, linking, testing, and scaling - much easier.

  • Use a Standard Layout: Start with a conventional directory structure (src, include, build, tests, libs). This separation keeps your project clean and understandable.
  • Separate Interface from Implementation: Use include/ for public headers that define your project's API and src/ for the .cpp files and private headers that contain the implementation details.
  • Organize for Scale: For larger projects, break your code into logical modules or libraries. Mirror this modular structure in your directories (e.g., src/core, src/graphics). This promotes separation of concerns and improves build times.
  • Embrace Out-of-Source Builds: Always keep your build artifacts separate from your source code by using a dedicated build/ directory.

With a well-organized project structure in place, we are now ready to start thinking about how to manage the code we didn't write. In the next lesson, we'll tackle the challenges of managing third-party libraries and external dependencies.

Have a question about this lesson?
Answers are generated by AI models and may not have been reviewed for accuracy