How to Structure Projects in C++ or Qt: CMake and Directories
Kaustuv Pokharel

Kaustuv Pokharel @kastuv

About: C++ is in the blood!

Joined:
Dec 25, 2023

How to Structure Projects in C++ or Qt: CMake and Directories

Publish Date: Jul 14
1 0

📺 Tutorial Playlist

Qt6 projects are built with CMake, and CMake plays a crucial role in managing and structuring C++ projects.


What is CMake?

CMake is a build system manager; it tells tools like Make, Ninja, or Visual Studio how to compile our C++ code, what libraries to use, where to find headers, or how to organize the output.

Note: CMake is not a compiler. It is just a build system generator. It creates the actual build files (like Makefile, Ninja files, Visual Studio solutions) that tell the compiler like g++, clang++, etc., how to build our C++ project.


One important term that keeps coming up when working with CMake is "target".

What is a target?

A target is the thing you're building.

What it Builds:

  • add_executable(): A complete program with main()
  • add_library(): A reusable module (static/shared)
  • Imported target: From system or library (like Qt6::Core)

Example:

add_executable(MyApp main.cpp)
Enter fullscreen mode Exit fullscreen mode

Here:

  • MyApp is a target name.
  • It's an executable target.
  • It will build into something like MyApp.exe or ./MyApp.

Why Targets Have Different Names in Folders?

Because each folder has its own role, so its target is named after what it's doing.

Example:

project/
├── src/
│   ├── Core/
│   ├── UI/
│   └── Network/
Enter fullscreen mode Exit fullscreen mode

Each has:

# src/Core/CMakeLists.txt
add_library(CoreLib STATIC CoreEngine.cpp)

# src/UI/CMakeLists.txt
add_library(UILib STATIC MainWindow.cpp)
Enter fullscreen mode Exit fullscreen mode

Then in root:

add_executable(MyApp main.cpp)
target_link_libraries(MyApp CoreLib UILib)
Enter fullscreen mode Exit fullscreen mode

So you get:

  • CoreLib – reusable backend logic
  • UILib – reusable UI components
  • MyApp – the final app that uses them

Why So Many target_* Commands?

Because you don't want to change things globally.
Instead, you do everything per-target, so your build is modular and clean.

Example:

target_include_directories(CoreLib PRIVATE include/)
target_compile_definitions(UILib PUBLIC USE_QT)
Enter fullscreen mode Exit fullscreen mode

What Happens When You Run CMake?

Step 1:

cmake -S . -B build/
Enter fullscreen mode Exit fullscreen mode

Step 2:

cmake --build build/
Enter fullscreen mode Exit fullscreen mode

What this does:

  • CMakeLists.txt is read.
  • It finds what to build (targets).
  • It finds what libraries and dependencies are needed.
  • It generates a Makefile or build.ninja or .vcxproj.
  • You then build using make or ninja or an IDE.

CMake Vocabulary You Must Know

cmake_minimum_required(VERSION <min_version>)

Sets the minimum version of CMake required to build the project.

cmake_minimum_required(VERSION 3.16)
Enter fullscreen mode Exit fullscreen mode

project(<project_name> VERSION <version> LANGUAGES <lang1> <lang2>)

Defines the project name, version, and language.

project(myApp VERSION 1.0 LANGUAGES CXX)
Enter fullscreen mode Exit fullscreen mode

set(<VAR_NAME> <value>)

Assigns a value to a variable.

set(CMAKE_CXX_STANDARD 20)
set(SOURCES main.cpp utils.cpp)
Enter fullscreen mode Exit fullscreen mode

You can use the variable as ${CMAKE_CXX_STANDARD} and ${SOURCES}


add_executable(<target_name> <source_files...>)

Declares an executable to build.

add_executable(myApp main.cpp utils.cpp)
Enter fullscreen mode Exit fullscreen mode

add_library(<target_name> STATIC|SHARED <source_files...>)

Creates a library (static or shared).

Note:

  • add_executable → builds .exe or .app (for apps with main())
  • add_library → builds .a or .so (for reusable code or shared modules)

target_link_libraries(<target_name> <libraries…>)

Links one target to other targets, frameworks, or libraries.

target_link_libraries(myApp Qt6::Core Qt6::Quick customLib)
Enter fullscreen mode Exit fullscreen mode

target_include_directories(<target> PUBLIC|PRIVATE <dirs...>)

Adds header paths for a target.

target_include_directories(customLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
Enter fullscreen mode Exit fullscreen mode

find_package(<PackageName> [REQUIRED] [COMPONENTS ...])

Finds system-installed packages like Qt, Boost, or OpenCV.

find_package(Qt6 REQUIRED COMPONENTS Core Quick Widgets)
Enter fullscreen mode Exit fullscreen mode

find_path(<VAR> <header> [PATHS path1 path2 ...])

Finds a directory containing a header.

find_path("path here" customLib.h)
Enter fullscreen mode Exit fullscreen mode

find_library(<VAR> <Library_name> [PATHS path1 path2 ...])

Finds the path to a compiled library (.a, .so, .lib)

find_library(MYLIB_LIBRARY myLib)
Enter fullscreen mode Exit fullscreen mode

add_subdirectory(<folder>)

Adds a subfolder that contains its own CMakeLists.txt.

add_subdirectory(src)
add_subdirectory(tests)
Enter fullscreen mode Exit fullscreen mode

install(TARGETS <target> DESTINATION <folder>)

install(FILES <file> DESTINATION <folder>)

Specifies install paths for targets or files.

install(TARGETS myApp DESTINATION bin)
Enter fullscreen mode Exit fullscreen mode

set_target_properties(<target> PROPERTIES <key> <value> ...)

Sets advanced properties like bundle info or output name.

set_target_properties(myApp PROPERTIES MACOSX_BUNDLE TRUE)
Enter fullscreen mode Exit fullscreen mode

message

Prints custom messages to the console during CMake configuration.

message(STATUS "Hello CMAKE")
Enter fullscreen mode Exit fullscreen mode

Modular Build System (Separation of Concerns)

What it means:

Split your project into independent parts (modules). Each part has its own job.

Example:

Imagine a project with:

  • UI/ — Handles how things look (buttons, windows)
  • Network/ — Handles internet communication
  • Utils/ — Handles helper functions

Each folder has its own CMakeLists.txt, so they can be built separately.

project/
├── UI/
│   ├── Window.qml
│   └── CMakeLists.txt
├── Network/
│   ├── NetManager.cpp
│   └── CMakeLists.txt
├── main.cpp
└── CMakeLists.txt
Enter fullscreen mode Exit fullscreen mode

Why it helps:

  • Makes each module easy to understand and update
  • You can focus on one part at a time
  • Faster Builds with Better Parallelism

Faster Builds with Better Parallelism

What it means:

When you change a file, only its module is rebuilt — not the whole project.

Example:

You change UI/Button.qml. Only the UI part needs to rebuild. The rest (like Network) is untouched.

# In UI/CMakeLists.txt
add_library(UI STATIC
    Button.qml
)
Enter fullscreen mode Exit fullscreen mode

Why it helps:

  • Saves time when compiling
  • Especially important in big projects with 100+ files

Clean Project Structure

What it means:

Keeps your project organized.

Example:

Bad:

project/
├── main.cpp
├── NetManager.cpp
├── Window.qml
├── AppUtils.cpp
Enter fullscreen mode Exit fullscreen mode

Good:

project/
├── UI/
├── Network/
├── Utils/
Enter fullscreen mode Exit fullscreen mode

Why it helps:

  • Easy to find things
  • Looks professional
  • Encourages teamwork (different people work on different folders)

Scoped Dependencies and Linking

What it means:

Only include what's necessary in each module.

Example:

The Network module needs Qt's network support. The UI module needs QtQuick.

# Network/CMakeLists.txt
target_link_libraries(Network Qt6::Network)

# UI/CMakeLists.txt
target_link_libraries(UI Qt6::Quick)
Enter fullscreen mode Exit fullscreen mode

Why it helps:

  • Smaller binary
  • Faster to compile
  • You avoid bloating the entire app with unused stuff

Testing and CI/CD Ease

What it means:

You can write and run tests for each module.

Example:

Utils/
├── MathHelper.cpp
├── CMakeLists.txt
└── tests/
    ├── test_MathHelper.cpp
    └── CMakeLists.txt
Enter fullscreen mode Exit fullscreen mode

Why it helps:

  • Test only what you need
  • Catch bugs early
  • Works great with GitHub Actions or GitLab CI

Supports Plugins and Scalability

What it means:

Big apps like QGroundControl support plugins. Each plugin is a subdirectory.

Example:

Plugins/
├── GPSPlugin/
│   ├── GPS.cpp
│   └── CMakeLists.txt
Enter fullscreen mode Exit fullscreen mode

In the main CMakeLists.txt:

add_subdirectory(Plugins/GPSPlugin)
Enter fullscreen mode Exit fullscreen mode

Why it helps:

  • Add or remove features easily
  • Users can build only what they need

Project structure example:

MyApp/
├── CMakeLists.txt        # Root
├── main.cpp
├── UI/
│   └── CMakeLists.txt
├── Network/
│   └── CMakeLists.txt
└── Utils/
    └── CMakeLists.txt
Enter fullscreen mode Exit fullscreen mode

Root CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(MyApp)

add_subdirectory(UI)
add_subdirectory(Network)
add_subdirectory(Utils)

add_executable(MyApp main.cpp)
target_link_libraries(MyApp
    UI
    Network
    Utils
    Qt6::Core
)
Enter fullscreen mode Exit fullscreen mode

UI/CMakeLists.txt:

add_library(UI STATIC
    MainWindow.qml
)
target_link_libraries(UI Qt6::Quick)
Enter fullscreen mode Exit fullscreen mode

Structuring your C++ or Qt project with CMake properly from the beginning will save you hours in debugging, rebuilding, and scaling. With clear module boundaries, proper target scopes, and clean linking, your project becomes professional, maintainable, and ready for collaboration or production.

Comments 0 total

    Add comment